toolkit/components/aboutmemory/content/aboutMemory.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/aboutmemory/content/aboutMemory.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1906 @@
     1.4 +/* -*- Mode: js2; tab-width: 8; indent-tabs-mode: nil; js2-basic-offset: 2 -*-*/
     1.5 +/* vim: set ts=8 sts=2 et sw=2 tw=80: */
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.9 +
    1.10 +// You can direct about:memory to immediately load memory reports from a file
    1.11 +// by providing a file= query string.  For example,
    1.12 +//
    1.13 +//     about:memory?file=/home/username/reports.json.gz
    1.14 +//
    1.15 +// "file=" is not case-sensitive.  We'll URI-unescape the contents of the
    1.16 +// "file=" argument, and obviously the filename is case-sensitive iff you're on
    1.17 +// a case-sensitive filesystem.  If you specify more than one "file=" argument,
    1.18 +// only the first one is used.
    1.19 +
    1.20 +"use strict";
    1.21 +
    1.22 +//---------------------------------------------------------------------------
    1.23 +
    1.24 +const Cc = Components.classes;
    1.25 +const Ci = Components.interfaces;
    1.26 +const Cu = Components.utils;
    1.27 +const CC = Components.Constructor;
    1.28 +
    1.29 +const KIND_NONHEAP           = Ci.nsIMemoryReporter.KIND_NONHEAP;
    1.30 +const KIND_HEAP              = Ci.nsIMemoryReporter.KIND_HEAP;
    1.31 +const KIND_OTHER             = Ci.nsIMemoryReporter.KIND_OTHER;
    1.32 +const UNITS_BYTES            = Ci.nsIMemoryReporter.UNITS_BYTES;
    1.33 +const UNITS_COUNT            = Ci.nsIMemoryReporter.UNITS_COUNT;
    1.34 +const UNITS_COUNT_CUMULATIVE = Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE;
    1.35 +const UNITS_PERCENTAGE       = Ci.nsIMemoryReporter.UNITS_PERCENTAGE;
    1.36 +
    1.37 +Cu.import("resource://gre/modules/Services.jsm");
    1.38 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.39 +
    1.40 +XPCOMUtils.defineLazyGetter(this, "nsBinaryStream",
    1.41 +                            () => CC("@mozilla.org/binaryinputstream;1",
    1.42 +                                     "nsIBinaryInputStream",
    1.43 +                                     "setInputStream"));
    1.44 +XPCOMUtils.defineLazyGetter(this, "nsFile",
    1.45 +                            () => CC("@mozilla.org/file/local;1",
    1.46 +                                     "nsIFile", "initWithPath"));
    1.47 +XPCOMUtils.defineLazyGetter(this, "nsGzipConverter",
    1.48 +                            () => CC("@mozilla.org/streamconv;1?from=gzip&to=uncompressed",
    1.49 +                                     "nsIStreamConverter"));
    1.50 +
    1.51 +let gMgr = Cc["@mozilla.org/memory-reporter-manager;1"]
    1.52 +             .getService(Ci.nsIMemoryReporterManager);
    1.53 +
    1.54 +let gUnnamedProcessStr = "Main Process";
    1.55 +
    1.56 +let gIsDiff = false;
    1.57 +
    1.58 +//---------------------------------------------------------------------------
    1.59 +
    1.60 +// Forward slashes in URLs in paths are represented with backslashes to avoid
    1.61 +// being mistaken for path separators.  Paths/names where this hasn't been
    1.62 +// undone are prefixed with "unsafe"; the rest are prefixed with "safe".
    1.63 +function flipBackslashes(aUnsafeStr)
    1.64 +{
    1.65 +  // Save memory by only doing the replacement if it's necessary.
    1.66 +  return (aUnsafeStr.indexOf('\\') === -1)
    1.67 +         ? aUnsafeStr
    1.68 +         : aUnsafeStr.replace(/\\/g, '/');
    1.69 +}
    1.70 +
    1.71 +const gAssertionFailureMsgPrefix = "aboutMemory.js assertion failed: ";
    1.72 +
    1.73 +// This is used for things that should never fail, and indicate a defect in
    1.74 +// this file if they do.
    1.75 +function assert(aCond, aMsg)
    1.76 +{
    1.77 +  if (!aCond) {
    1.78 +    reportAssertionFailure(aMsg)
    1.79 +    throw(gAssertionFailureMsgPrefix + aMsg);
    1.80 +  }
    1.81 +}
    1.82 +
    1.83 +// This is used for malformed input from memory reporters.
    1.84 +function assertInput(aCond, aMsg)
    1.85 +{
    1.86 +  if (!aCond) {
    1.87 +    throw "Invalid memory report(s): " + aMsg;
    1.88 +  }
    1.89 +}
    1.90 +
    1.91 +function handleException(ex)
    1.92 +{
    1.93 +  let str = ex.toString();
    1.94 +  if (str.startsWith(gAssertionFailureMsgPrefix)) {
    1.95 +    // Argh, assertion failure within this file!  Give up.
    1.96 +    throw ex;
    1.97 +  } else {
    1.98 +    // File or memory reporter problem.  Print a message.
    1.99 +    updateMainAndFooter(ex.toString(), HIDE_FOOTER, "badInputWarning");
   1.100 +  }
   1.101 +}
   1.102 +
   1.103 +function reportAssertionFailure(aMsg)
   1.104 +{
   1.105 +  let debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
   1.106 +  if (debug.isDebugBuild) {
   1.107 +    debug.assertion(aMsg, "false", "aboutMemory.js", 0);
   1.108 +  }
   1.109 +}
   1.110 +
   1.111 +function debug(x)
   1.112 +{
   1.113 +  let section = appendElement(document.body, 'div', 'section');
   1.114 +  appendElementWithText(section, "div", "debug", JSON.stringify(x));
   1.115 +}
   1.116 +
   1.117 +//---------------------------------------------------------------------------
   1.118 +
   1.119 +function onUnload()
   1.120 +{
   1.121 +}
   1.122 +
   1.123 +//---------------------------------------------------------------------------
   1.124 +
   1.125 +// The <div> holding everything but the header and footer (if they're present).
   1.126 +// It's what is updated each time the page changes.
   1.127 +let gMain;
   1.128 +
   1.129 +// The <div> holding the footer.
   1.130 +let gFooter;
   1.131 +
   1.132 +// The "verbose" checkbox.
   1.133 +let gVerbose;
   1.134 +
   1.135 +// Values for the second argument to updateMainAndFooter.
   1.136 +let HIDE_FOOTER = 0;
   1.137 +let SHOW_FOOTER = 1;
   1.138 +
   1.139 +function updateMainAndFooter(aMsg, aFooterAction, aClassName)
   1.140 +{
   1.141 +  // Clear gMain by replacing it with an empty node.
   1.142 +  let tmp = gMain.cloneNode(false);
   1.143 +  gMain.parentNode.replaceChild(tmp, gMain);
   1.144 +  gMain = tmp;
   1.145 +
   1.146 +  gMain.classList.remove('hidden');
   1.147 +  gMain.classList.remove('verbose');
   1.148 +  gMain.classList.remove('non-verbose');
   1.149 +  if (gVerbose) {
   1.150 +    gMain.classList.add(gVerbose.checked ? 'verbose' : 'non-verbose');
   1.151 +  }
   1.152 +
   1.153 +  if (aMsg) {
   1.154 +    let className = "section"
   1.155 +    if (aClassName) {
   1.156 +      className = className + " " + aClassName;
   1.157 +    }
   1.158 +    appendElementWithText(gMain, 'div', className, aMsg);
   1.159 +  }
   1.160 +
   1.161 +  switch (aFooterAction) {
   1.162 +   case HIDE_FOOTER:   gFooter.classList.add('hidden');    break;
   1.163 +   case SHOW_FOOTER:   gFooter.classList.remove('hidden'); break;
   1.164 +   default: assertInput(false, "bad footer action in updateMainAndFooter");
   1.165 +  }
   1.166 +}
   1.167 +
   1.168 +function appendTextNode(aP, aText)
   1.169 +{
   1.170 +  let e = document.createTextNode(aText);
   1.171 +  aP.appendChild(e);
   1.172 +  return e;
   1.173 +}
   1.174 +
   1.175 +function appendElement(aP, aTagName, aClassName)
   1.176 +{
   1.177 +  let e = document.createElement(aTagName);
   1.178 +  if (aClassName) {
   1.179 +    e.className = aClassName;
   1.180 +  }
   1.181 +  aP.appendChild(e);
   1.182 +  return e;
   1.183 +}
   1.184 +
   1.185 +function appendElementWithText(aP, aTagName, aClassName, aText)
   1.186 +{
   1.187 +  let e = appendElement(aP, aTagName, aClassName);
   1.188 +  // Setting textContent clobbers existing children, but there are none.  More
   1.189 +  // importantly, it avoids creating a JS-land object for the node, saving
   1.190 +  // memory.
   1.191 +  e.textContent = aText;
   1.192 +  return e;
   1.193 +}
   1.194 +
   1.195 +//---------------------------------------------------------------------------
   1.196 +
   1.197 +const explicitTreeDescription =
   1.198 +"This tree covers explicit memory allocations by the application.  It includes \
   1.199 +\n\n\
   1.200 +* allocations made at the operating system level (via calls to functions such as \
   1.201 +VirtualAlloc, vm_allocate, and mmap), \
   1.202 +\n\n\
   1.203 +* allocations made at the heap allocation level (via functions such as malloc, \
   1.204 +calloc, realloc, memalign, operator new, and operator new[]) that have not been \
   1.205 +explicitly decommitted (i.e. evicted from memory and swap), and \
   1.206 +\n\n\
   1.207 +* where possible, the overhead of the heap allocator itself.\
   1.208 +\n\n\
   1.209 +It excludes memory that is mapped implicitly such as code and data segments, \
   1.210 +and thread stacks. \
   1.211 +\n\n\
   1.212 +'explicit' is not guaranteed to cover every explicit allocation, but it does cover \
   1.213 +most (including the entire heap), and therefore it is the single best number to \
   1.214 +focus on when trying to reduce memory usage.";
   1.215 +
   1.216 +//---------------------------------------------------------------------------
   1.217 +
   1.218 +function appendButton(aP, aTitle, aOnClick, aText, aId)
   1.219 +{
   1.220 +  let b = appendElementWithText(aP, "button", "", aText);
   1.221 +  b.title = aTitle;
   1.222 +  b.onclick = aOnClick;
   1.223 +  if (aId) {
   1.224 +    b.id = aId;
   1.225 +  }
   1.226 +  return b;
   1.227 +}
   1.228 +
   1.229 +function appendHiddenFileInput(aP, aId, aChangeListener)
   1.230 +{
   1.231 +  let input = appendElementWithText(aP, "input", "hidden", "");
   1.232 +  input.type = "file";
   1.233 +  input.id = aId;      // used in testing
   1.234 +  input.addEventListener("change", aChangeListener);
   1.235 +  return input;
   1.236 +}
   1.237 +
   1.238 +function onLoad()
   1.239 +{
   1.240 +  // Generate the header.
   1.241 +
   1.242 +  let header = appendElement(document.body, "div", "ancillary");
   1.243 +
   1.244 +  // A hidden file input element that can be invoked when necessary.
   1.245 +  let fileInput1 = appendHiddenFileInput(header, "fileInput1", function() {
   1.246 +    let file = this.files[0];
   1.247 +    let filename = file.mozFullPath;
   1.248 +    updateAboutMemoryFromFile(filename);
   1.249 +  });
   1.250 +
   1.251 +  // Ditto.
   1.252 +  let fileInput2 =
   1.253 +      appendHiddenFileInput(header, "fileInput2", function(e) {
   1.254 +    let file = this.files[0];
   1.255 +    // First time around, we stash a copy of the filename and reinvoke.  Second
   1.256 +    // time around we do the diff and display.
   1.257 +    if (!this.filename1) {
   1.258 +      this.filename1 = file.mozFullPath;
   1.259 +
   1.260 +      // e.skipClick is only true when testing -- it allows fileInput2's
   1.261 +      // onchange handler to be re-called without having to go via the file
   1.262 +      // picker.
   1.263 +      if (!e.skipClick) {
   1.264 +        this.click();
   1.265 +      }
   1.266 +    } else {
   1.267 +      let filename1 = this.filename1;
   1.268 +      delete this.filename1;
   1.269 +      updateAboutMemoryFromTwoFiles(filename1, file.mozFullPath);
   1.270 +    }
   1.271 +  });
   1.272 +
   1.273 +  const CuDesc = "Measure current memory reports and show.";
   1.274 +  const LdDesc = "Load memory reports from file and show.";
   1.275 +  const DfDesc = "Load memory report data from two files and show the " +
   1.276 +                 "difference.";
   1.277 +  const RdDesc = "Read memory reports from the clipboard and show.";
   1.278 +
   1.279 +  const SvDesc = "Save memory reports to file.";
   1.280 +
   1.281 +  const GCDesc = "Do a global garbage collection.";
   1.282 +  const CCDesc = "Do a cycle collection.";
   1.283 +  const MMDesc = "Send three \"heap-minimize\" notifications in a " +
   1.284 +                 "row.  Each notification triggers a global garbage " +
   1.285 +                 "collection followed by a cycle collection, and causes the " +
   1.286 +                 "process to reduce memory usage in other ways, e.g. by " +
   1.287 +                 "flushing various caches.";
   1.288 +
   1.289 +  const GCAndCCLogDesc = "Save garbage collection log and concise cycle " +
   1.290 +                         "collection log.\n" +
   1.291 +                         "WARNING: These logs may be large (>1GB).";
   1.292 +  const GCAndCCAllLogDesc = "Save garbage collection log and verbose cycle " +
   1.293 +                            "collection log.\n" +
   1.294 +                            "WARNING: These logs may be large (>1GB).";
   1.295 +
   1.296 +  let ops = appendElement(header, "div", "");
   1.297 +
   1.298 +  let row1 = appendElement(ops, "div", "opsRow");
   1.299 +
   1.300 +  let labelDiv =
   1.301 +   appendElementWithText(row1, "div", "opsRowLabel", "Show memory reports");
   1.302 +  let label = appendElementWithText(labelDiv, "label", "");
   1.303 +  gVerbose = appendElement(label, "input", "");
   1.304 +  gVerbose.type = "checkbox";
   1.305 +  gVerbose.id = "verbose";   // used for testing
   1.306 +
   1.307 +  appendTextNode(label, "verbose");
   1.308 +
   1.309 +  const kEllipsis = "\u2026";
   1.310 +
   1.311 +  // The "measureButton" id is used for testing.
   1.312 +  appendButton(row1, CuDesc, doMeasure, "Measure", "measureButton");
   1.313 +  appendButton(row1, LdDesc, () => fileInput1.click(), "Load" + kEllipsis);
   1.314 +  appendButton(row1, DfDesc, () => fileInput2.click(),
   1.315 +               "Load and diff" + kEllipsis);
   1.316 +  appendButton(row1, RdDesc, updateAboutMemoryFromClipboard,
   1.317 +               "Read from clipboard");
   1.318 +
   1.319 +  let row2 = appendElement(ops, "div", "opsRow");
   1.320 +
   1.321 +  appendElementWithText(row2, "div", "opsRowLabel", "Save memory reports");
   1.322 +  appendButton(row2, SvDesc, saveReportsToFile, "Measure and save" + kEllipsis);
   1.323 +
   1.324 +  let row3 = appendElement(ops, "div", "opsRow");
   1.325 +
   1.326 +  appendElementWithText(row3, "div", "opsRowLabel", "Free memory");
   1.327 +  appendButton(row3, GCDesc, doGC,  "GC");
   1.328 +  appendButton(row3, CCDesc, doCC,  "CC");
   1.329 +  appendButton(row3, MMDesc, doMMU, "Minimize memory usage");
   1.330 +
   1.331 +  let row4 = appendElement(ops, "div", "opsRow");
   1.332 +
   1.333 +  appendElementWithText(row4, "div", "opsRowLabel", "Save GC & CC logs");
   1.334 +  appendButton(row4, GCAndCCLogDesc,
   1.335 +               saveGCLogAndConciseCCLog, "Save concise", 'saveLogsConcise');
   1.336 +  appendButton(row4, GCAndCCAllLogDesc,
   1.337 +               saveGCLogAndVerboseCCLog, "Save verbose", 'saveLogsVerbose');
   1.338 +
   1.339 +  // Generate the main div, where content ("section" divs) will go.  It's
   1.340 +  // hidden at first.
   1.341 +
   1.342 +  gMain = appendElement(document.body, 'div', '');
   1.343 +  gMain.id = 'mainDiv';
   1.344 +
   1.345 +  // Generate the footer.  It's hidden at first.
   1.346 +
   1.347 +  gFooter = appendElement(document.body, 'div', 'ancillary hidden');
   1.348 +
   1.349 +  let a = appendElementWithText(gFooter, "a", "option",
   1.350 +                                "Troubleshooting information");
   1.351 +  a.href = "about:support";
   1.352 +
   1.353 +  let legendText1 = "Click on a non-leaf node in a tree to expand ('++') " +
   1.354 +                    "or collapse ('--') its children.";
   1.355 +  let legendText2 = "Hover the pointer over the name of a memory report " +
   1.356 +                    "to see a description of what it measures.";
   1.357 +
   1.358 +  appendElementWithText(gFooter, "div", "legend", legendText1);
   1.359 +  appendElementWithText(gFooter, "div", "legend hiddenOnMobile", legendText2);
   1.360 +
   1.361 +  // See if we're loading from a file.  (Because about:memory is a non-standard
   1.362 +  // URL, location.search is undefined, so we have to use location.href
   1.363 +  // instead.)
   1.364 +  let search = location.href.split('?')[1];
   1.365 +  if (search) {
   1.366 +    let searchSplit = search.split('&');
   1.367 +    for (let i = 0; i < searchSplit.length; i++) {
   1.368 +      if (searchSplit[i].toLowerCase().startsWith('file=')) {
   1.369 +        let filename = searchSplit[i].substring('file='.length);
   1.370 +        updateAboutMemoryFromFile(decodeURIComponent(filename));
   1.371 +        return;
   1.372 +      }
   1.373 +    }
   1.374 +  }
   1.375 +}
   1.376 +
   1.377 +//---------------------------------------------------------------------------
   1.378 +
   1.379 +function doGC()
   1.380 +{
   1.381 +  Services.obs.notifyObservers(null, "child-gc-request", null);
   1.382 +  Cu.forceGC();
   1.383 +  updateMainAndFooter("Garbage collection completed", HIDE_FOOTER);
   1.384 +}
   1.385 +
   1.386 +function doCC()
   1.387 +{
   1.388 +  Services.obs.notifyObservers(null, "child-cc-request", null);
   1.389 +  window.QueryInterface(Ci.nsIInterfaceRequestor)
   1.390 +        .getInterface(Ci.nsIDOMWindowUtils)
   1.391 +        .cycleCollect();
   1.392 +  updateMainAndFooter("Cycle collection completed", HIDE_FOOTER);
   1.393 +}
   1.394 +
   1.395 +function doMMU()
   1.396 +{
   1.397 +  Services.obs.notifyObservers(null, "child-mmu-request", null);
   1.398 +  gMgr.minimizeMemoryUsage(
   1.399 +    () => updateMainAndFooter("Memory minimization completed", HIDE_FOOTER));
   1.400 +}
   1.401 +
   1.402 +function doMeasure()
   1.403 +{
   1.404 +  updateAboutMemoryFromReporters();
   1.405 +}
   1.406 +
   1.407 +function saveGCLogAndConciseCCLog()
   1.408 +{
   1.409 +  dumpGCLogAndCCLog(false);
   1.410 +}
   1.411 +
   1.412 +function saveGCLogAndVerboseCCLog()
   1.413 +{
   1.414 +  dumpGCLogAndCCLog(true);
   1.415 +}
   1.416 +
   1.417 +function dumpGCLogAndCCLog(aVerbose)
   1.418 +{
   1.419 +  let gcLogPath = {};
   1.420 +  let ccLogPath = {};
   1.421 +
   1.422 +  let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
   1.423 +                .getService(Ci.nsIMemoryInfoDumper);
   1.424 +
   1.425 +  updateMainAndFooter("Saving logs...", HIDE_FOOTER);
   1.426 +
   1.427 +  dumper.dumpGCAndCCLogsToFile("", aVerbose, /* dumpChildProcesses = */ false,
   1.428 +                               gcLogPath, ccLogPath);
   1.429 +
   1.430 +  updateMainAndFooter("", HIDE_FOOTER);
   1.431 +  let section = appendElement(gMain, 'div', "section");
   1.432 +  appendElementWithText(section, 'div', "",
   1.433 +                        "Saved GC log to " + gcLogPath.value);
   1.434 +
   1.435 +  let ccLogType = aVerbose ? "verbose" : "concise";
   1.436 +  appendElementWithText(section, 'div', "",
   1.437 +                        "Saved " + ccLogType + " CC log to " + ccLogPath.value);
   1.438 +}
   1.439 +
   1.440 +/**
   1.441 + * Top-level function that does the work of generating the page from the memory
   1.442 + * reporters.
   1.443 + */
   1.444 +function updateAboutMemoryFromReporters()
   1.445 +{
   1.446 +  updateMainAndFooter("Measuring...", HIDE_FOOTER);
   1.447 +
   1.448 +  try {
   1.449 +    let processLiveMemoryReports =
   1.450 +        function(aHandleReport, aDisplayReports) {
   1.451 +      let handleReport = function(aProcess, aUnsafePath, aKind, aUnits,
   1.452 +                                  aAmount, aDescription) {
   1.453 +        aHandleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
   1.454 +                      aDescription, /* presence = */ undefined);
   1.455 +      }
   1.456 +
   1.457 +      let displayReportsAndFooter = function() {
   1.458 +        updateMainAndFooter("", SHOW_FOOTER);
   1.459 +        aDisplayReports();
   1.460 +      }
   1.461 +
   1.462 +      gMgr.getReports(handleReport, null,
   1.463 +                      displayReportsAndFooter, null);
   1.464 +    }
   1.465 +
   1.466 +    // Process the reports from the live memory reporters.
   1.467 +    appendAboutMemoryMain(processLiveMemoryReports,
   1.468 +                          gMgr.hasMozMallocUsableSize);
   1.469 +
   1.470 +  } catch (ex) {
   1.471 +    handleException(ex);
   1.472 +  }
   1.473 +}
   1.474 +
   1.475 +// Increment this if the JSON format changes.
   1.476 +//
   1.477 +var gCurrentFileFormatVersion = 1;
   1.478 +
   1.479 +/**
   1.480 + * Populate about:memory using the data in the given JSON object.
   1.481 + *
   1.482 + * @param aObj
   1.483 + *        An object containing JSON data that (hopefully!) conforms to the
   1.484 + *        schema used by nsIMemoryInfoDumper.
   1.485 + */
   1.486 +function updateAboutMemoryFromJSONObject(aObj)
   1.487 +{
   1.488 +  try {
   1.489 +    assertInput(aObj.version === gCurrentFileFormatVersion,
   1.490 +                "data version number missing or doesn't match");
   1.491 +    assertInput(aObj.hasMozMallocUsableSize !== undefined,
   1.492 +                "missing 'hasMozMallocUsableSize' property");
   1.493 +    assertInput(aObj.reports && aObj.reports instanceof Array,
   1.494 +                "missing or non-array 'reports' property");
   1.495 +
   1.496 +    let processMemoryReportsFromFile =
   1.497 +        function(aHandleReport, aDisplayReports) {
   1.498 +      for (let i = 0; i < aObj.reports.length; i++) {
   1.499 +        let r = aObj.reports[i];
   1.500 +
   1.501 +        // A hack: for a brief time (late in the FF26 and early in the FF27
   1.502 +        // cycle) we were dumping memory report files that contained reports
   1.503 +        // whose path began with "redundant/".  Such reports were ignored by
   1.504 +        // about:memory.  These reports are no longer produced, but some older
   1.505 +        // builds are still floating around and producing files that contain
   1.506 +        // them, so we need to still handle them (i.e. ignore them).  This hack
   1.507 +        // can be removed once FF26 and associated products (e.g. B2G 1.2) are
   1.508 +        // no longer in common use.
   1.509 +        if (!r.path.startsWith("redundant/")) {
   1.510 +          aHandleReport(r.process, r.path, r.kind, r.units, r.amount,
   1.511 +                        r.description, r._presence);
   1.512 +        }
   1.513 +      }
   1.514 +      aDisplayReports();
   1.515 +    }
   1.516 +    appendAboutMemoryMain(processMemoryReportsFromFile,
   1.517 +                          aObj.hasMozMallocUsableSize);
   1.518 +  } catch (ex) {
   1.519 +    handleException(ex);
   1.520 +  }
   1.521 +}
   1.522 +
   1.523 +/**
   1.524 + * Populate about:memory using the data in the given JSON string.
   1.525 + *
   1.526 + * @param aStr
   1.527 + *        A string containing JSON data conforming to the schema used by
   1.528 + *        nsIMemoryReporterManager::dumpReports.
   1.529 + */
   1.530 +function updateAboutMemoryFromJSONString(aStr)
   1.531 +{
   1.532 +  try {
   1.533 +    let obj = JSON.parse(aStr);
   1.534 +    updateAboutMemoryFromJSONObject(obj);
   1.535 +  } catch (ex) {
   1.536 +    handleException(ex);
   1.537 +  }
   1.538 +}
   1.539 +
   1.540 +/**
   1.541 + * Loads the contents of a file into a string and passes that to a callback.
   1.542 + *
   1.543 + * @param aFilename
   1.544 + *        The name of the file being read from.
   1.545 + * @param aFn
   1.546 + *        The function to call and pass the read string to upon completion.
   1.547 + */
   1.548 +function loadMemoryReportsFromFile(aFilename, aFn)
   1.549 +{
   1.550 +  updateMainAndFooter("Loading...", HIDE_FOOTER);
   1.551 +
   1.552 +  try {
   1.553 +    let reader = new FileReader();
   1.554 +    reader.onerror = () => { throw "FileReader.onerror"; };
   1.555 +    reader.onabort = () => { throw "FileReader.onabort"; };
   1.556 +    reader.onload = (aEvent) => {
   1.557 +      updateMainAndFooter("", SHOW_FOOTER);  // Clear "Loading..." from above.
   1.558 +      aFn(aEvent.target.result);
   1.559 +    };
   1.560 +
   1.561 +    // If it doesn't have a .gz suffix, read it as a (legacy) ungzipped file.
   1.562 +    if (!aFilename.endsWith(".gz")) {
   1.563 +      reader.readAsText(new File(aFilename));
   1.564 +      return;
   1.565 +    }
   1.566 +
   1.567 +    // Read compressed gzip file.
   1.568 +    let converter = new nsGzipConverter();
   1.569 +    converter.asyncConvertData("gzip", "uncompressed", {
   1.570 +      data: [],
   1.571 +      onStartRequest: function(aR, aC) {},
   1.572 +      onDataAvailable: function(aR, aC, aStream, aO, aCount) {
   1.573 +        let bi = new nsBinaryStream(aStream);
   1.574 +        this.data.push(bi.readBytes(aCount));
   1.575 +      },
   1.576 +      onStopRequest: function(aR, aC, aStatusCode) {
   1.577 +        try {
   1.578 +          if (!Components.isSuccessCode(aStatusCode)) {
   1.579 +            throw aStatusCode;
   1.580 +          }
   1.581 +          reader.readAsText(new Blob(this.data));
   1.582 +        } catch (ex) {
   1.583 +          handleException(ex);
   1.584 +        }
   1.585 +      }
   1.586 +    }, null);
   1.587 +
   1.588 +    let file = new nsFile(aFilename);
   1.589 +    let fileChan = Services.io.newChannelFromURI(Services.io.newFileURI(file));
   1.590 +    fileChan.asyncOpen(converter, null);
   1.591 +
   1.592 +  } catch (ex) {
   1.593 +    handleException(ex);
   1.594 +  }
   1.595 +}
   1.596 +
   1.597 +/**
   1.598 + * Like updateAboutMemoryFromReporters(), but gets its data from a file instead
   1.599 + * of the memory reporters.
   1.600 + *
   1.601 + * @param aFilename
   1.602 + *        The name of the file being read from.  The expected format of the
   1.603 + *        file's contents is described in a comment in nsIMemoryInfoDumper.idl.
   1.604 + */
   1.605 +function updateAboutMemoryFromFile(aFilename)
   1.606 +{
   1.607 +  loadMemoryReportsFromFile(aFilename,
   1.608 +                            updateAboutMemoryFromJSONString);
   1.609 +}
   1.610 +
   1.611 +/**
   1.612 + * Like updateAboutMemoryFromFile(), but gets its data from a two files and
   1.613 + * diffs them.
   1.614 + *
   1.615 + * @param aFilename1
   1.616 + *        The name of the first file being read from.
   1.617 + * @param aFilename2
   1.618 + *        The name of the first file being read from.
   1.619 + */
   1.620 +function updateAboutMemoryFromTwoFiles(aFilename1, aFilename2)
   1.621 +{
   1.622 +  loadMemoryReportsFromFile(aFilename1, function(aStr1) {
   1.623 +    loadMemoryReportsFromFile(aFilename2, function f2(aStr2) {
   1.624 +      try {
   1.625 +        let obj1 = JSON.parse(aStr1);
   1.626 +        let obj2 = JSON.parse(aStr2);
   1.627 +        gIsDiff = true;
   1.628 +        updateAboutMemoryFromJSONObject(diffJSONObjects(obj1, obj2));
   1.629 +        gIsDiff = false;
   1.630 +      } catch (ex) {
   1.631 +        handleException(ex);
   1.632 +      }
   1.633 +    });
   1.634 +  });
   1.635 +}
   1.636 +
   1.637 +/**
   1.638 + * Like updateAboutMemoryFromFile(), but gets its data from the clipboard
   1.639 + * instead of a file.
   1.640 + */
   1.641 +function updateAboutMemoryFromClipboard()
   1.642 +{
   1.643 +  // Get the clipboard's contents.
   1.644 +  let transferable = Cc["@mozilla.org/widget/transferable;1"]
   1.645 +                       .createInstance(Ci.nsITransferable);
   1.646 +  let loadContext = window.QueryInterface(Ci.nsIInterfaceRequestor)
   1.647 +                          .getInterface(Ci.nsIWebNavigation)
   1.648 +                          .QueryInterface(Ci.nsILoadContext);
   1.649 +  transferable.init(loadContext);
   1.650 +  transferable.addDataFlavor('text/unicode');
   1.651 +  Services.clipboard.getData(transferable, Ci.nsIClipboard.kGlobalClipboard);
   1.652 +
   1.653 +  var cbData = {};
   1.654 +  try {
   1.655 +    transferable.getTransferData('text/unicode', cbData,
   1.656 +                                 /* out dataLen (ignored) */ {});
   1.657 +    let cbString = cbData.value.QueryInterface(Ci.nsISupportsString).data;
   1.658 +
   1.659 +    // Success!  Now use the string to generate about:memory.
   1.660 +    updateAboutMemoryFromJSONString(cbString);
   1.661 +
   1.662 +  } catch (ex) {
   1.663 +    handleException(ex);
   1.664 +  }
   1.665 +}
   1.666 +
   1.667 +//---------------------------------------------------------------------------
   1.668 +
   1.669 +// Something unlikely to appear in a process name.
   1.670 +let kProcessPathSep = "^:^:^";
   1.671 +
   1.672 +// Short for "diff report".
   1.673 +function DReport(aKind, aUnits, aAmount, aDescription, aNMerged, aPresence)
   1.674 +{
   1.675 +  this._kind = aKind;
   1.676 +  this._units = aUnits;
   1.677 +  this._amount = aAmount;
   1.678 +  this._description = aDescription;
   1.679 +  this._nMerged = aNMerged;
   1.680 +  if (aPresence !== undefined) {
   1.681 +    this._presence = aPresence;
   1.682 +  }
   1.683 +}
   1.684 +
   1.685 +DReport.prototype = {
   1.686 +  assertCompatible: function(aKind, aUnits)
   1.687 +  {
   1.688 +    assert(this._kind  == aKind,  "Mismatched kinds");
   1.689 +    assert(this._units == aUnits, "Mismatched units");
   1.690 +
   1.691 +    // We don't check that the "description" properties match.  This is because
   1.692 +    // on Linux we can get cases where the paths are the same but the
   1.693 +    // descriptions differ, like this:
   1.694 +    //
   1.695 +    //   "path": "size/other-files/icon-theme.cache/[r--p]",
   1.696 +    //   "description": "/usr/share/icons/gnome/icon-theme.cache (read-only, not executable, private)"
   1.697 +    //
   1.698 +    //   "path": "size/other-files/icon-theme.cache/[r--p]"
   1.699 +    //   "description": "/usr/share/icons/hicolor/icon-theme.cache (read-only, not executable, private)"
   1.700 +    //
   1.701 +    // In those cases, we just use the description from the first-encountered
   1.702 +    // one, which is what about:memory also does.
   1.703 +    // (Note: reports with those paths are no longer generated, but allowing
   1.704 +    // the descriptions to differ seems reasonable.)
   1.705 +  },
   1.706 +
   1.707 +  merge: function(aJr) {
   1.708 +    this.assertCompatible(aJr.kind, aJr.units);
   1.709 +    this._amount += aJr.amount;
   1.710 +    this._nMerged++;
   1.711 +  },
   1.712 +
   1.713 +  toJSON: function(aProcess, aPath, aAmount) {
   1.714 +    return {
   1.715 +      process:     aProcess,
   1.716 +      path:        aPath,
   1.717 +      kind:        this._kind,
   1.718 +      units:       this._units,
   1.719 +      amount:      aAmount,
   1.720 +      description: this._description,
   1.721 +      _presence:   this._presence
   1.722 +    };
   1.723 +  }
   1.724 +};
   1.725 +
   1.726 +// Constants that indicate if a DReport was present only in one of the data
   1.727 +// sets, or had to be added for balance.
   1.728 +DReport.PRESENT_IN_FIRST_ONLY  = 1;
   1.729 +DReport.PRESENT_IN_SECOND_ONLY = 2;
   1.730 +DReport.ADDED_FOR_BALANCE = 3;
   1.731 +
   1.732 +/**
   1.733 + * Make a report map, which has combined path+process strings for keys, and
   1.734 + * DReport objects for values.
   1.735 + *
   1.736 + * @param aJSONReports
   1.737 + *        The |reports| field of a JSON object.
   1.738 + * @return The constructed report map.
   1.739 + */
   1.740 +function makeDReportMap(aJSONReports)
   1.741 +{
   1.742 +  let dreportMap = {};
   1.743 +  for (let i = 0; i < aJSONReports.length; i++) {
   1.744 +    let jr = aJSONReports[i];
   1.745 +
   1.746 +    assert(jr.process     !== undefined, "Missing process");
   1.747 +    assert(jr.path        !== undefined, "Missing path");
   1.748 +    assert(jr.kind        !== undefined, "Missing kind");
   1.749 +    assert(jr.units       !== undefined, "Missing units");
   1.750 +    assert(jr.amount      !== undefined, "Missing amount");
   1.751 +    assert(jr.description !== undefined, "Missing description");
   1.752 +
   1.753 +    // Strip out some non-deterministic stuff that prevents clean diffs --
   1.754 +    // e.g. PIDs, addresses.
   1.755 +    let strippedProcess = jr.process.replace(/pid \d+/, "pid NNN");
   1.756 +    let strippedPath = jr.path.replace(/0x[0-9A-Fa-f]+/, "0xNNN");
   1.757 +    let processPath = strippedProcess + kProcessPathSep + strippedPath;
   1.758 +
   1.759 +    let rOld = dreportMap[processPath];
   1.760 +    if (rOld === undefined) {
   1.761 +      dreportMap[processPath] =
   1.762 +        new DReport(jr.kind, jr.units, jr.amount, jr.description, 1, undefined);
   1.763 +    } else {
   1.764 +      rOld.merge(jr);
   1.765 +    }
   1.766 +  }
   1.767 +  return dreportMap;
   1.768 +}
   1.769 +
   1.770 +// Return a new dreportMap which is the diff of two dreportMaps.  Empties
   1.771 +// aDReportMap2 along the way.
   1.772 +function diffDReportMaps(aDReportMap1, aDReportMap2)
   1.773 +{
   1.774 +  let result = {};
   1.775 +
   1.776 +  for (let processPath in aDReportMap1) {
   1.777 +    let r1 = aDReportMap1[processPath];
   1.778 +    let r2 = aDReportMap2[processPath];
   1.779 +    let r2_amount, r2_nMerged;
   1.780 +    let presence;
   1.781 +    if (r2 !== undefined) {
   1.782 +      r1.assertCompatible(r2._kind, r2._units);
   1.783 +      r2_amount = r2._amount;
   1.784 +      r2_nMerged = r2._nMerged;
   1.785 +      delete aDReportMap2[processPath];
   1.786 +      presence = undefined;   // represents that it's present in both
   1.787 +    } else {
   1.788 +      r2_amount = 0;
   1.789 +      r2_nMerged = 0;
   1.790 +      presence = DReport.PRESENT_IN_FIRST_ONLY;
   1.791 +    }
   1.792 +    result[processPath] =
   1.793 +      new DReport(r1._kind, r1._units, r2_amount - r1._amount, r1._description,
   1.794 +                  Math.max(r1._nMerged, r2_nMerged), presence);
   1.795 +  }
   1.796 +
   1.797 +  for (let processPath in aDReportMap2) {
   1.798 +    let r2 = aDReportMap2[processPath];
   1.799 +    result[processPath] = new DReport(r2._kind, r2._units, r2._amount,
   1.800 +                                      r2._description, r2._nMerged,
   1.801 +                                      DReport.PRESENT_IN_SECOND_ONLY);
   1.802 +  }
   1.803 +
   1.804 +  return result;
   1.805 +}
   1.806 +
   1.807 +function makeJSONReports(aDReportMap)
   1.808 +{
   1.809 +  let reports = [];
   1.810 +  for (let processPath in aDReportMap) {
   1.811 +    let r = aDReportMap[processPath];
   1.812 +    if (r._amount !== 0) {
   1.813 +      // If _nMerged > 1, we give the full (aggregated) amount in the first
   1.814 +      // copy, and then use amount=0 in the remainder.  When viewed in
   1.815 +      // about:memory, this shows up as an entry with a "[2]"-style suffix
   1.816 +      // and the correct amount.
   1.817 +      let split = processPath.split(kProcessPathSep);
   1.818 +      assert(split.length >= 2);
   1.819 +      let process = split.shift();
   1.820 +      let path = split.join();
   1.821 +      reports.push(r.toJSON(process, path, r._amount));
   1.822 +      for (let i = 1; i < r._nMerged; i++) {
   1.823 +        reports.push(r.toJSON(process, path, 0));
   1.824 +      }
   1.825 +    }
   1.826 +  }
   1.827 +
   1.828 +  return reports;
   1.829 +}
   1.830 +
   1.831 +
   1.832 +// Diff two JSON objects holding memory reports.
   1.833 +function diffJSONObjects(aJson1, aJson2)
   1.834 +{
   1.835 +  function simpleProp(aProp)
   1.836 +  {
   1.837 +    assert(aJson1[aProp] !== undefined && aJson1[aProp] === aJson2[aProp],
   1.838 +           aProp + " properties don't match");
   1.839 +    return aJson1[aProp];
   1.840 +  }
   1.841 +
   1.842 +  return {
   1.843 +    version: simpleProp("version"),
   1.844 +
   1.845 +    hasMozMallocUsableSize: simpleProp("hasMozMallocUsableSize"),
   1.846 +
   1.847 +    reports: makeJSONReports(diffDReportMaps(makeDReportMap(aJson1.reports),
   1.848 +                                             makeDReportMap(aJson2.reports)))
   1.849 +  };
   1.850 +}
   1.851 +
   1.852 +//---------------------------------------------------------------------------
   1.853 +
   1.854 +// |PColl| is short for "process collection".
   1.855 +function PColl()
   1.856 +{
   1.857 +  this._trees = {};
   1.858 +  this._degenerates = {};
   1.859 +  this._heapTotal = 0;
   1.860 +}
   1.861 +
   1.862 +/**
   1.863 + * Processes reports (whether from reporters or from a file) and append the
   1.864 + * main part of the page.
   1.865 + *
   1.866 + * @param aProcessReports
   1.867 + *        Function that extracts the memory reports from the reporters or from
   1.868 + *        file.
   1.869 + * @param aHasMozMallocUsableSize
   1.870 + *        Boolean indicating if moz_malloc_usable_size works.
   1.871 + */
   1.872 +function appendAboutMemoryMain(aProcessReports, aHasMozMallocUsableSize)
   1.873 +{
   1.874 +  let pcollsByProcess = {};
   1.875 +
   1.876 +  function handleReport(aProcess, aUnsafePath, aKind, aUnits, aAmount,
   1.877 +                        aDescription, aPresence)
   1.878 +  {
   1.879 +    if (aUnsafePath.startsWith("explicit/")) {
   1.880 +      assertInput(aKind === KIND_HEAP || aKind === KIND_NONHEAP,
   1.881 +                  "bad explicit kind");
   1.882 +      assertInput(aUnits === UNITS_BYTES, "bad explicit units");
   1.883 +    }
   1.884 +
   1.885 +    assert(aPresence === undefined ||
   1.886 +           aPresence == DReport.PRESENT_IN_FIRST_ONLY ||
   1.887 +           aPresence == DReport.PRESENT_IN_SECOND_ONLY,
   1.888 +           "bad presence");
   1.889 +
   1.890 +    let process = aProcess === "" ? gUnnamedProcessStr : aProcess;
   1.891 +    let unsafeNames = aUnsafePath.split('/');
   1.892 +    let unsafeName0 = unsafeNames[0];
   1.893 +    let isDegenerate = unsafeNames.length === 1;
   1.894 +
   1.895 +    // Get the PColl table for the process, creating it if necessary.
   1.896 +    let pcoll = pcollsByProcess[process];
   1.897 +    if (!pcollsByProcess[process]) {
   1.898 +      pcoll = pcollsByProcess[process] = new PColl();
   1.899 +    }
   1.900 +
   1.901 +    // Get the root node, creating it if necessary.
   1.902 +    let psubcoll = isDegenerate ? pcoll._degenerates : pcoll._trees;
   1.903 +    let t = psubcoll[unsafeName0];
   1.904 +    if (!t) {
   1.905 +      t = psubcoll[unsafeName0] =
   1.906 +        new TreeNode(unsafeName0, aUnits, isDegenerate);
   1.907 +    }
   1.908 +
   1.909 +    if (!isDegenerate) {
   1.910 +      // Add any missing nodes in the tree implied by aUnsafePath, and fill in
   1.911 +      // the properties that we can with a top-down traversal.
   1.912 +      for (let i = 1; i < unsafeNames.length; i++) {
   1.913 +        let unsafeName = unsafeNames[i];
   1.914 +        let u = t.findKid(unsafeName);
   1.915 +        if (!u) {
   1.916 +          u = new TreeNode(unsafeName, aUnits, isDegenerate);
   1.917 +          if (!t._kids) {
   1.918 +            t._kids = [];
   1.919 +          }
   1.920 +          t._kids.push(u);
   1.921 +        }
   1.922 +        t = u;
   1.923 +      }
   1.924 +
   1.925 +      // Update the heap total if necessary.
   1.926 +      if (unsafeName0 === "explicit" && aKind == KIND_HEAP) {
   1.927 +        pcollsByProcess[process]._heapTotal += aAmount;
   1.928 +      }
   1.929 +    }
   1.930 +
   1.931 +    if (t._amount) {
   1.932 +      // Duplicate!  Sum the values and mark it as a dup.
   1.933 +      t._amount += aAmount;
   1.934 +      t._nMerged = t._nMerged ? t._nMerged + 1 : 2;
   1.935 +      assert(t._presence === aPresence, "presence mismatch");
   1.936 +    } else {
   1.937 +      // New leaf node.  Fill in extra node details from the report.
   1.938 +      t._amount = aAmount;
   1.939 +      t._description = aDescription;
   1.940 +      if (aPresence !== undefined) {
   1.941 +        t._presence = aPresence;
   1.942 +      }
   1.943 +    }
   1.944 +  }
   1.945 +
   1.946 +  function displayReports()
   1.947 +  {
   1.948 +    // Sort the processes.
   1.949 +    let processes = Object.keys(pcollsByProcess);
   1.950 +    processes.sort(function(aProcessA, aProcessB) {
   1.951 +      assert(aProcessA != aProcessB,
   1.952 +             "Elements of Object.keys() should be unique, but " +
   1.953 +             "saw duplicate '" + aProcessA + "' elem.");
   1.954 +
   1.955 +      // Always put the main process first.
   1.956 +      if (aProcessA == gUnnamedProcessStr) {
   1.957 +        return -1;
   1.958 +      }
   1.959 +      if (aProcessB == gUnnamedProcessStr) {
   1.960 +        return 1;
   1.961 +      }
   1.962 +
   1.963 +      // Then sort by resident size.
   1.964 +      let nodeA = pcollsByProcess[aProcessA]._degenerates['resident'];
   1.965 +      let nodeB = pcollsByProcess[aProcessB]._degenerates['resident'];
   1.966 +      let residentA = nodeA ? nodeA._amount : -1;
   1.967 +      let residentB = nodeB ? nodeB._amount : -1;
   1.968 +
   1.969 +      if (residentA > residentB) {
   1.970 +        return -1;
   1.971 +      }
   1.972 +      if (residentA < residentB) {
   1.973 +        return 1;
   1.974 +      }
   1.975 +
   1.976 +      // Then sort by process name.
   1.977 +      if (aProcessA < aProcessB) {
   1.978 +        return -1;
   1.979 +      }
   1.980 +      if (aProcessA > aProcessB) {
   1.981 +        return 1;
   1.982 +      }
   1.983 +
   1.984 +      return 0;
   1.985 +    });
   1.986 +
   1.987 +    // Generate output for each process.
   1.988 +    for (let i = 0; i < processes.length; i++) {
   1.989 +      let process = processes[i];
   1.990 +      let section = appendElement(gMain, 'div', 'section');
   1.991 +
   1.992 +      appendProcessAboutMemoryElements(section, i, process,
   1.993 +                                       pcollsByProcess[process]._trees,
   1.994 +                                       pcollsByProcess[process]._degenerates,
   1.995 +                                       pcollsByProcess[process]._heapTotal,
   1.996 +                                       aHasMozMallocUsableSize);
   1.997 +    }
   1.998 +  }
   1.999 +
  1.1000 +  aProcessReports(handleReport, displayReports);
  1.1001 +}
  1.1002 +
  1.1003 +//---------------------------------------------------------------------------
  1.1004 +
  1.1005 +// There are two kinds of TreeNode.
  1.1006 +// - Leaf TreeNodes correspond to reports.
  1.1007 +// - Non-leaf TreeNodes are just scaffolding nodes for the tree;  their values
  1.1008 +//   are derived from their children.
  1.1009 +// Some trees are "degenerate", i.e. they contain a single node, i.e. they
  1.1010 +// correspond to a report whose path has no '/' separators.
  1.1011 +function TreeNode(aUnsafeName, aUnits, aIsDegenerate)
  1.1012 +{
  1.1013 +  this._units = aUnits;
  1.1014 +  this._unsafeName = aUnsafeName;
  1.1015 +  if (aIsDegenerate) {
  1.1016 +    this._isDegenerate = true;
  1.1017 +  }
  1.1018 +
  1.1019 +  // Leaf TreeNodes have these properties added immediately after construction:
  1.1020 +  // - _amount
  1.1021 +  // - _description
  1.1022 +  // - _nMerged (only defined if > 1)
  1.1023 +  // - _presence (only defined if value is PRESENT_IN_{FIRST,SECOND}_ONLY)
  1.1024 +  //
  1.1025 +  // Non-leaf TreeNodes have these properties added later:
  1.1026 +  // - _kids
  1.1027 +  // - _amount
  1.1028 +  // - _description
  1.1029 +  // - _hideKids (only defined if true)
  1.1030 +}
  1.1031 +
  1.1032 +TreeNode.prototype = {
  1.1033 +  findKid: function(aUnsafeName) {
  1.1034 +    if (this._kids) {
  1.1035 +      for (let i = 0; i < this._kids.length; i++) {
  1.1036 +        if (this._kids[i]._unsafeName === aUnsafeName) {
  1.1037 +          return this._kids[i];
  1.1038 +        }
  1.1039 +      }
  1.1040 +    }
  1.1041 +    return undefined;
  1.1042 +  },
  1.1043 +
  1.1044 +  toString: function() {
  1.1045 +    switch (this._units) {
  1.1046 +      case UNITS_BYTES:            return formatBytes(this._amount);
  1.1047 +      case UNITS_COUNT:
  1.1048 +      case UNITS_COUNT_CUMULATIVE: return formatInt(this._amount);
  1.1049 +      case UNITS_PERCENTAGE:       return formatPercentage(this._amount);
  1.1050 +      default:
  1.1051 +        assertInput(false, "bad units in TreeNode.toString");
  1.1052 +    }
  1.1053 +  }
  1.1054 +};
  1.1055 +
  1.1056 +// Sort TreeNodes first by size, then by name.  This is particularly important
  1.1057 +// for the about:memory tests, which need a predictable ordering of reporters
  1.1058 +// which have the same amount.
  1.1059 +TreeNode.compareAmounts = function(aA, aB) {
  1.1060 +  let a, b;
  1.1061 +  if (gIsDiff) {
  1.1062 +    a = Math.abs(aA._amount);
  1.1063 +    b = Math.abs(aB._amount);
  1.1064 +  } else {
  1.1065 +    a = aA._amount;
  1.1066 +    b = aB._amount;
  1.1067 +  }
  1.1068 +  if (a > b) {
  1.1069 +    return -1;
  1.1070 +  }
  1.1071 +  if (a < b) {
  1.1072 +    return 1;
  1.1073 +  }
  1.1074 +  return TreeNode.compareUnsafeNames(aA, aB);
  1.1075 +};
  1.1076 +
  1.1077 +TreeNode.compareUnsafeNames = function(aA, aB) {
  1.1078 +  return aA._unsafeName < aB._unsafeName ? -1 :
  1.1079 +         aA._unsafeName > aB._unsafeName ?  1 :
  1.1080 +         0;
  1.1081 +};
  1.1082 +
  1.1083 +
  1.1084 +/**
  1.1085 + * Fill in the remaining properties for the specified tree in a bottom-up
  1.1086 + * fashion.
  1.1087 + *
  1.1088 + * @param aRoot
  1.1089 + *        The tree root.
  1.1090 + */
  1.1091 +function fillInTree(aRoot)
  1.1092 +{
  1.1093 +  // Fill in the remaining properties bottom-up.
  1.1094 +  function fillInNonLeafNodes(aT)
  1.1095 +  {
  1.1096 +    if (!aT._kids) {
  1.1097 +      // Leaf node.  Has already been filled in.
  1.1098 +
  1.1099 +    } else if (aT._kids.length === 1 && aT != aRoot) {
  1.1100 +      // Non-root, non-leaf node with one child.  Merge the child with the node
  1.1101 +      // to avoid redundant entries.
  1.1102 +      let kid = aT._kids[0];
  1.1103 +      let kidBytes = fillInNonLeafNodes(kid);
  1.1104 +      aT._unsafeName += '/' + kid._unsafeName;
  1.1105 +      if (kid._kids) {
  1.1106 +        aT._kids = kid._kids;
  1.1107 +      } else {
  1.1108 +        delete aT._kids;
  1.1109 +      }
  1.1110 +      aT._amount = kid._amount;
  1.1111 +      aT._description = kid._description;
  1.1112 +      if (kid._nMerged !== undefined) {
  1.1113 +        aT._nMerged = kid._nMerged
  1.1114 +      }
  1.1115 +      assert(!aT._hideKids && !kid._hideKids, "_hideKids set when merging");
  1.1116 +
  1.1117 +    } else {
  1.1118 +      // Non-leaf node with multiple children.  Derive its _amount and
  1.1119 +      // _description entirely from its children...
  1.1120 +      let kidsBytes = 0;
  1.1121 +      for (let i = 0; i < aT._kids.length; i++) {
  1.1122 +        kidsBytes += fillInNonLeafNodes(aT._kids[i]);
  1.1123 +      }
  1.1124 +
  1.1125 +      // ... except in one special case. When diffing two memory report sets,
  1.1126 +      // if one set has a node with children and the other has the same node
  1.1127 +      // but without children -- e.g. the first has "a/b/c" and "a/b/d", but
  1.1128 +      // the second only has "a/b" -- we need to add a fake node "a/b/(fake)"
  1.1129 +      // to the second to make the trees comparable. It's ugly, but it works.
  1.1130 +      if (aT._amount !== undefined &&
  1.1131 +          (aT._presence === DReport.PRESENT_IN_FIRST_ONLY ||
  1.1132 +           aT._presence === DReport.PRESENT_IN_SECOND_ONLY)) {
  1.1133 +        aT._amount += kidsBytes;
  1.1134 +        let fake = new TreeNode('(fake child)', aT._units);
  1.1135 +        fake._presence = DReport.ADDED_FOR_BALANCE;
  1.1136 +        fake._amount = aT._amount - kidsBytes;
  1.1137 +        aT._kids.push(fake);
  1.1138 +        delete aT._presence;
  1.1139 +      } else {
  1.1140 +        assert(aT._amount === undefined,
  1.1141 +               "_amount already set for non-leaf node")
  1.1142 +        aT._amount = kidsBytes;
  1.1143 +      }
  1.1144 +      aT._description = "The sum of all entries below this one.";
  1.1145 +    }
  1.1146 +    return aT._amount;
  1.1147 +  }
  1.1148 +
  1.1149 +  // cannotMerge is set because don't want to merge into a tree's root node.
  1.1150 +  fillInNonLeafNodes(aRoot);
  1.1151 +}
  1.1152 +
  1.1153 +/**
  1.1154 + * Compute the "heap-unclassified" value and insert it into the "explicit"
  1.1155 + * tree.
  1.1156 + *
  1.1157 + * @param aT
  1.1158 + *        The "explicit" tree.
  1.1159 + * @param aHeapAllocatedNode
  1.1160 + *        The "heap-allocated" tree node.
  1.1161 + * @param aHeapTotal
  1.1162 + *        The sum of all explicit HEAP reports for this process.
  1.1163 + * @return A boolean indicating if "heap-allocated" is known for the process.
  1.1164 + */
  1.1165 +function addHeapUnclassifiedNode(aT, aHeapAllocatedNode, aHeapTotal)
  1.1166 +{
  1.1167 +  if (aHeapAllocatedNode === undefined)
  1.1168 +    return false;
  1.1169 +
  1.1170 +  assert(aHeapAllocatedNode._isDegenerate, "heap-allocated is not degenerate");
  1.1171 +  let heapAllocatedBytes = aHeapAllocatedNode._amount;
  1.1172 +  let heapUnclassifiedT = new TreeNode("heap-unclassified", UNITS_BYTES);
  1.1173 +  heapUnclassifiedT._amount = heapAllocatedBytes - aHeapTotal;
  1.1174 +  heapUnclassifiedT._description =
  1.1175 +      "Memory not classified by a more specific report. This includes " +
  1.1176 +      "slop bytes due to internal fragmentation in the heap allocator " +
  1.1177 +      "(caused when the allocator rounds up request sizes).";
  1.1178 +  aT._kids.push(heapUnclassifiedT);
  1.1179 +  aT._amount += heapUnclassifiedT._amount;
  1.1180 +  return true;
  1.1181 +}
  1.1182 +
  1.1183 +/**
  1.1184 + * Sort all kid nodes from largest to smallest, and insert aggregate nodes
  1.1185 + * where appropriate.
  1.1186 + *
  1.1187 + * @param aTotalBytes
  1.1188 + *        The size of the tree's root node.
  1.1189 + * @param aT
  1.1190 + *        The tree.
  1.1191 + */
  1.1192 +function sortTreeAndInsertAggregateNodes(aTotalBytes, aT)
  1.1193 +{
  1.1194 +  const kSignificanceThresholdPerc = 1;
  1.1195 +
  1.1196 +  function isInsignificant(aT)
  1.1197 +  {
  1.1198 +    return !gVerbose.checked &&
  1.1199 +           (100 * aT._amount / aTotalBytes) < kSignificanceThresholdPerc;
  1.1200 +  }
  1.1201 +
  1.1202 +  if (!aT._kids) {
  1.1203 +    return;
  1.1204 +  }
  1.1205 +
  1.1206 +  aT._kids.sort(TreeNode.compareAmounts);
  1.1207 +
  1.1208 +  // If the first child is insignificant, they all are, and there's no point
  1.1209 +  // creating an aggregate node that lacks siblings.  Just set the parent's
  1.1210 +  // _hideKids property and process all children.
  1.1211 +  if (isInsignificant(aT._kids[0])) {
  1.1212 +    aT._hideKids = true;
  1.1213 +    for (let i = 0; i < aT._kids.length; i++) {
  1.1214 +      sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
  1.1215 +    }
  1.1216 +    return;
  1.1217 +  }
  1.1218 +
  1.1219 +  // Look at all children except the last one.
  1.1220 +  let i;
  1.1221 +  for (i = 0; i < aT._kids.length - 1; i++) {
  1.1222 +    if (isInsignificant(aT._kids[i])) {
  1.1223 +      // This child is below the significance threshold.  If there are other
  1.1224 +      // (smaller) children remaining, move them under an aggregate node.
  1.1225 +      let i0 = i;
  1.1226 +      let nAgg = aT._kids.length - i0;
  1.1227 +      // Create an aggregate node.  Inherit units from the parent;  everything
  1.1228 +      // in the tree should have the same units anyway (we test this later).
  1.1229 +      let aggT = new TreeNode("(" + nAgg + " tiny)", aT._units);
  1.1230 +      aggT._kids = [];
  1.1231 +      let aggBytes = 0;
  1.1232 +      for ( ; i < aT._kids.length; i++) {
  1.1233 +        aggBytes += aT._kids[i]._amount;
  1.1234 +        aggT._kids.push(aT._kids[i]);
  1.1235 +      }
  1.1236 +      aggT._hideKids = true;
  1.1237 +      aggT._amount = aggBytes;
  1.1238 +      aggT._description =
  1.1239 +        nAgg + " sub-trees that are below the " + kSignificanceThresholdPerc +
  1.1240 +        "% significance threshold.";
  1.1241 +      aT._kids.splice(i0, nAgg, aggT);
  1.1242 +      aT._kids.sort(TreeNode.compareAmounts);
  1.1243 +
  1.1244 +      // Process the moved children.
  1.1245 +      for (i = 0; i < aggT._kids.length; i++) {
  1.1246 +        sortTreeAndInsertAggregateNodes(aTotalBytes, aggT._kids[i]);
  1.1247 +      }
  1.1248 +      return;
  1.1249 +    }
  1.1250 +
  1.1251 +    sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
  1.1252 +  }
  1.1253 +
  1.1254 +  // The first n-1 children were significant.  Don't consider if the last child
  1.1255 +  // is significant;  there's no point creating an aggregate node that only has
  1.1256 +  // one child.  Just process it.
  1.1257 +  sortTreeAndInsertAggregateNodes(aTotalBytes, aT._kids[i]);
  1.1258 +}
  1.1259 +
  1.1260 +// Global variable indicating if we've seen any invalid values for this
  1.1261 +// process;  it holds the unsafePaths of any such reports.  It is reset for
  1.1262 +// each new process.
  1.1263 +let gUnsafePathsWithInvalidValuesForThisProcess = [];
  1.1264 +
  1.1265 +function appendWarningElements(aP, aHasKnownHeapAllocated,
  1.1266 +                               aHasMozMallocUsableSize)
  1.1267 +{
  1.1268 +  if (!aHasKnownHeapAllocated && !aHasMozMallocUsableSize) {
  1.1269 +    appendElementWithText(aP, "p", "",
  1.1270 +      "WARNING: the 'heap-allocated' memory reporter and the " +
  1.1271 +      "moz_malloc_usable_size() function do not work for this platform " +
  1.1272 +      "and/or configuration.  This means that 'heap-unclassified' is not " +
  1.1273 +      "shown and the 'explicit' tree shows much less memory than it should.\n\n");
  1.1274 +
  1.1275 +  } else if (!aHasKnownHeapAllocated) {
  1.1276 +    appendElementWithText(aP, "p", "",
  1.1277 +      "WARNING: the 'heap-allocated' memory reporter does not work for this " +
  1.1278 +      "platform and/or configuration. This means that 'heap-unclassified' " +
  1.1279 +      "is not shown and the 'explicit' tree shows less memory than it should.\n\n");
  1.1280 +
  1.1281 +  } else if (!aHasMozMallocUsableSize) {
  1.1282 +    appendElementWithText(aP, "p", "",
  1.1283 +      "WARNING: the moz_malloc_usable_size() function does not work for " +
  1.1284 +      "this platform and/or configuration.  This means that much of the " +
  1.1285 +      "heap-allocated memory is not measured by individual memory reporters " +
  1.1286 +      "and so will fall under 'heap-unclassified'.\n\n");
  1.1287 +  }
  1.1288 +
  1.1289 +  if (gUnsafePathsWithInvalidValuesForThisProcess.length > 0) {
  1.1290 +    let div = appendElement(aP, "div");
  1.1291 +    appendElementWithText(div, "p", "",
  1.1292 +      "WARNING: the following values are negative or unreasonably large.\n");
  1.1293 +
  1.1294 +    let ul = appendElement(div, "ul");
  1.1295 +    for (let i = 0;
  1.1296 +         i < gUnsafePathsWithInvalidValuesForThisProcess.length;
  1.1297 +         i++)
  1.1298 +    {
  1.1299 +      appendTextNode(ul, " ");
  1.1300 +      appendElementWithText(ul, "li", "",
  1.1301 +        flipBackslashes(gUnsafePathsWithInvalidValuesForThisProcess[i]) + "\n");
  1.1302 +    }
  1.1303 +
  1.1304 +    appendElementWithText(div, "p", "",
  1.1305 +      "This indicates a defect in one or more memory reporters.  The " +
  1.1306 +      "invalid values are highlighted.\n\n");
  1.1307 +    gUnsafePathsWithInvalidValuesForThisProcess = [];  // reset for the next process
  1.1308 +  }
  1.1309 +}
  1.1310 +
  1.1311 +/**
  1.1312 + * Appends the about:memory elements for a single process.
  1.1313 + *
  1.1314 + * @param aP
  1.1315 + *        The parent DOM node.
  1.1316 + * @param aN
  1.1317 + *        The number of the process, starting at 0.
  1.1318 + * @param aProcess
  1.1319 + *        The name of the process.
  1.1320 + * @param aTrees
  1.1321 + *        The table of non-degenerate trees for this process.
  1.1322 + * @param aDegenerates
  1.1323 + *        The table of degenerate trees for this process.
  1.1324 + * @param aHasMozMallocUsableSize
  1.1325 + *        Boolean indicating if moz_malloc_usable_size works.
  1.1326 + * @return The generated text.
  1.1327 + */
  1.1328 +function appendProcessAboutMemoryElements(aP, aN, aProcess, aTrees,
  1.1329 +                                          aDegenerates, aHeapTotal,
  1.1330 +                                          aHasMozMallocUsableSize)
  1.1331 +{
  1.1332 +  const kUpwardsArrow   = "\u2191",
  1.1333 +        kDownwardsArrow = "\u2193";
  1.1334 +
  1.1335 +  let appendLink = function(aHere, aThere, aArrow) {
  1.1336 +    let link = appendElementWithText(aP, "a", "upDownArrow", aArrow);
  1.1337 +    link.href = "#" + aThere + aN;
  1.1338 +    link.id = aHere + aN;
  1.1339 +    link.title = "Go to the " + aThere + " of " + aProcess;
  1.1340 +    link.style = "text-decoration: none";
  1.1341 +
  1.1342 +    // This jumps to the anchor without the page location getting the anchor
  1.1343 +    // name tacked onto its end, which is what happens with a vanilla link.
  1.1344 +    link.addEventListener("click", function(event) {
  1.1345 +      document.documentElement.scrollTop =
  1.1346 +        document.querySelector(event.target.href).offsetTop;
  1.1347 +      event.preventDefault();
  1.1348 +    }, false);
  1.1349 +
  1.1350 +    // This gives nice spacing when we copy and paste.
  1.1351 +    appendElementWithText(aP, "span", "", "\n");
  1.1352 +  }
  1.1353 +
  1.1354 +  appendElementWithText(aP, "h1", "", aProcess);
  1.1355 +  appendLink("start", "end", kDownwardsArrow);
  1.1356 +
  1.1357 +  // We'll fill this in later.
  1.1358 +  let warningsDiv = appendElement(aP, "div", "accuracyWarning");
  1.1359 +
  1.1360 +  // The explicit tree.
  1.1361 +  let hasExplicitTree;
  1.1362 +  let hasKnownHeapAllocated;
  1.1363 +  {
  1.1364 +    let treeName = "explicit";
  1.1365 +    let t = aTrees[treeName];
  1.1366 +    if (t) {
  1.1367 +      let pre = appendSectionHeader(aP, "Explicit Allocations");
  1.1368 +      hasExplicitTree = true;
  1.1369 +      fillInTree(t);
  1.1370 +      // Using the "heap-allocated" reporter here instead of
  1.1371 +      // nsMemoryReporterManager.heapAllocated goes against the usual pattern.
  1.1372 +      // But the "heap-allocated" node will go in the tree like the others, so
  1.1373 +      // we have to deal with it, and once we're dealing with it, it's easier
  1.1374 +      // to keep doing so rather than switching to the distinguished amount.
  1.1375 +      hasKnownHeapAllocated =
  1.1376 +        aDegenerates &&
  1.1377 +        addHeapUnclassifiedNode(t, aDegenerates["heap-allocated"], aHeapTotal);
  1.1378 +      sortTreeAndInsertAggregateNodes(t._amount, t);
  1.1379 +      t._description = explicitTreeDescription;
  1.1380 +      appendTreeElements(pre, t, aProcess, "");
  1.1381 +      delete aTrees[treeName];
  1.1382 +    }
  1.1383 +    appendTextNode(aP, "\n");  // gives nice spacing when we copy and paste
  1.1384 +  }
  1.1385 +
  1.1386 +  // Fill in and sort all the non-degenerate other trees.
  1.1387 +  let otherTrees = [];
  1.1388 +  for (let unsafeName in aTrees) {
  1.1389 +    let t = aTrees[unsafeName];
  1.1390 +    assert(!t._isDegenerate, "tree is degenerate");
  1.1391 +    fillInTree(t);
  1.1392 +    sortTreeAndInsertAggregateNodes(t._amount, t);
  1.1393 +    otherTrees.push(t);
  1.1394 +  }
  1.1395 +  otherTrees.sort(TreeNode.compareUnsafeNames);
  1.1396 +
  1.1397 +  // Get the length of the longest root value among the degenerate other trees,
  1.1398 +  // and sort them as well.
  1.1399 +  let otherDegenerates = [];
  1.1400 +  let maxStringLength = 0;
  1.1401 +  for (let unsafeName in aDegenerates) {
  1.1402 +    let t = aDegenerates[unsafeName];
  1.1403 +    assert(t._isDegenerate, "tree is not degenerate");
  1.1404 +    let length = t.toString().length;
  1.1405 +    if (length > maxStringLength) {
  1.1406 +      maxStringLength = length;
  1.1407 +    }
  1.1408 +    otherDegenerates.push(t);
  1.1409 +  }
  1.1410 +  otherDegenerates.sort(TreeNode.compareUnsafeNames);
  1.1411 +
  1.1412 +  // Now generate the elements, putting non-degenerate trees first.
  1.1413 +  let pre = appendSectionHeader(aP, "Other Measurements");
  1.1414 +  for (let i = 0; i < otherTrees.length; i++) {
  1.1415 +    let t = otherTrees[i];
  1.1416 +    appendTreeElements(pre, t, aProcess, "");
  1.1417 +    appendTextNode(pre, "\n");  // blank lines after non-degenerate trees
  1.1418 +  }
  1.1419 +  for (let i = 0; i < otherDegenerates.length; i++) {
  1.1420 +    let t = otherDegenerates[i];
  1.1421 +    let padText = pad("", maxStringLength - t.toString().length, ' ');
  1.1422 +    appendTreeElements(pre, t, aProcess, padText);
  1.1423 +  }
  1.1424 +  appendTextNode(aP, "\n");  // gives nice spacing when we copy and paste
  1.1425 +
  1.1426 +  // Add any warnings about inaccuracies in the "explicit" tree due to platform
  1.1427 +  // limitations.  These must be computed after generating all the text.  The
  1.1428 +  // newlines give nice spacing if we copy+paste into a text buffer.
  1.1429 +  if (hasExplicitTree) {
  1.1430 +    appendWarningElements(warningsDiv, hasKnownHeapAllocated,
  1.1431 +                          aHasMozMallocUsableSize);
  1.1432 +  }
  1.1433 +
  1.1434 +  appendElementWithText(aP, "h3", "", "End of " + aProcess);
  1.1435 +  appendLink("end", "start", kUpwardsArrow);
  1.1436 +}
  1.1437 +
  1.1438 +/**
  1.1439 + * Determines if a number has a negative sign when converted to a string.
  1.1440 + * Works even for -0.
  1.1441 + *
  1.1442 + * @param aN
  1.1443 + *        The number.
  1.1444 + * @return A boolean.
  1.1445 + */
  1.1446 +function hasNegativeSign(aN)
  1.1447 +{
  1.1448 +  if (aN === 0) {                   // this succeeds for 0 and -0
  1.1449 +    return 1 / aN === -Infinity;    // this succeeds for -0
  1.1450 +  }
  1.1451 +  return aN < 0;
  1.1452 +}
  1.1453 +
  1.1454 +/**
  1.1455 + * Formats an int as a human-readable string.
  1.1456 + *
  1.1457 + * @param aN
  1.1458 + *        The integer to format.
  1.1459 + * @param aExtra
  1.1460 + *        An extra string to tack onto the end.
  1.1461 + * @return A human-readable string representing the int.
  1.1462 + *
  1.1463 + * Note: building an array of chars and converting that to a string with
  1.1464 + * Array.join at the end is more memory efficient than using string
  1.1465 + * concatenation.  See bug 722972 for details.
  1.1466 + */
  1.1467 +function formatInt(aN, aExtra)
  1.1468 +{
  1.1469 +  let neg = false;
  1.1470 +  if (hasNegativeSign(aN)) {
  1.1471 +    neg = true;
  1.1472 +    aN = -aN;
  1.1473 +  }
  1.1474 +  let s = [];
  1.1475 +  while (true) {
  1.1476 +    let k = aN % 1000;
  1.1477 +    aN = Math.floor(aN / 1000);
  1.1478 +    if (aN > 0) {
  1.1479 +      if (k < 10) {
  1.1480 +        s.unshift(",00", k);
  1.1481 +      } else if (k < 100) {
  1.1482 +        s.unshift(",0", k);
  1.1483 +      } else {
  1.1484 +        s.unshift(",", k);
  1.1485 +      }
  1.1486 +    } else {
  1.1487 +      s.unshift(k);
  1.1488 +      break;
  1.1489 +    }
  1.1490 +  }
  1.1491 +  if (neg) {
  1.1492 +    s.unshift("-");
  1.1493 +  }
  1.1494 +  if (aExtra) {
  1.1495 +    s.push(aExtra);
  1.1496 +  }
  1.1497 +  return s.join("");
  1.1498 +}
  1.1499 +
  1.1500 +/**
  1.1501 + * Converts a byte count to an appropriate string representation.
  1.1502 + *
  1.1503 + * @param aBytes
  1.1504 + *        The byte count.
  1.1505 + * @return The string representation.
  1.1506 + */
  1.1507 +function formatBytes(aBytes)
  1.1508 +{
  1.1509 +  let unit = gVerbose.checked ? " B" : " MB";
  1.1510 +
  1.1511 +  let s;
  1.1512 +  if (gVerbose.checked) {
  1.1513 +    s = formatInt(aBytes, unit);
  1.1514 +  } else {
  1.1515 +    let mbytes = (aBytes / (1024 * 1024)).toFixed(2);
  1.1516 +    let a = String(mbytes).split(".");
  1.1517 +    // If the argument to formatInt() is -0, it will print the negative sign.
  1.1518 +    s = formatInt(Number(a[0])) + "." + a[1] + unit;
  1.1519 +  }
  1.1520 +  return s;
  1.1521 +}
  1.1522 +
  1.1523 +/**
  1.1524 + * Converts a percentage to an appropriate string representation.
  1.1525 + *
  1.1526 + * @param aPerc100x
  1.1527 + *        The percentage, multiplied by 100 (see nsIMemoryReporter).
  1.1528 + * @return The string representation
  1.1529 + */
  1.1530 +function formatPercentage(aPerc100x)
  1.1531 +{
  1.1532 +  return (aPerc100x / 100).toFixed(2) + "%";
  1.1533 +}
  1.1534 +
  1.1535 +/**
  1.1536 + * Right-justifies a string in a field of a given width, padding as necessary.
  1.1537 + *
  1.1538 + * @param aS
  1.1539 + *        The string.
  1.1540 + * @param aN
  1.1541 + *        The field width.
  1.1542 + * @param aC
  1.1543 + *        The char used to pad.
  1.1544 + * @return The string representation.
  1.1545 + */
  1.1546 +function pad(aS, aN, aC)
  1.1547 +{
  1.1548 +  let padding = "";
  1.1549 +  let n2 = aN - aS.length;
  1.1550 +  for (let i = 0; i < n2; i++) {
  1.1551 +    padding += aC;
  1.1552 +  }
  1.1553 +  return padding + aS;
  1.1554 +}
  1.1555 +
  1.1556 +// There's a subset of the Unicode "light" box-drawing chars that is widely
  1.1557 +// implemented in terminals, and this code sticks to that subset to maximize
  1.1558 +// the chance that copying and pasting about:memory output to a terminal will
  1.1559 +// work correctly.
  1.1560 +const kHorizontal                   = "\u2500",
  1.1561 +      kVertical                     = "\u2502",
  1.1562 +      kUpAndRight                   = "\u2514",
  1.1563 +      kUpAndRight_Right_Right       = "\u2514\u2500\u2500",
  1.1564 +      kVerticalAndRight             = "\u251c",
  1.1565 +      kVerticalAndRight_Right_Right = "\u251c\u2500\u2500",
  1.1566 +      kVertical_Space_Space         = "\u2502  ";
  1.1567 +
  1.1568 +const kNoKidsSep                    = " \u2500\u2500 ",
  1.1569 +      kHideKidsSep                  = " ++ ",
  1.1570 +      kShowKidsSep                  = " -- ";
  1.1571 +
  1.1572 +function appendMrNameSpan(aP, aDescription, aUnsafeName, aIsInvalid, aNMerged,
  1.1573 +                          aPresence)
  1.1574 +{
  1.1575 +  let safeName = flipBackslashes(aUnsafeName);
  1.1576 +  if (!aIsInvalid && !aNMerged && !aPresence) {
  1.1577 +    safeName += "\n";
  1.1578 +  }
  1.1579 +  let nameSpan = appendElementWithText(aP, "span", "mrName", safeName);
  1.1580 +  nameSpan.title = aDescription;
  1.1581 +
  1.1582 +  if (aIsInvalid) {
  1.1583 +    let noteText = " [?!]";
  1.1584 +    if (!aNMerged) {
  1.1585 +      noteText += "\n";
  1.1586 +    }
  1.1587 +    let noteSpan = appendElementWithText(aP, "span", "mrNote", noteText);
  1.1588 +    noteSpan.title =
  1.1589 +      "Warning: this value is invalid and indicates a bug in one or more " +
  1.1590 +      "memory reporters. ";
  1.1591 +  }
  1.1592 +
  1.1593 +  if (aNMerged) {
  1.1594 +    let noteText = " [" + aNMerged + "]";
  1.1595 +    if (!aPresence) {
  1.1596 +      noteText += "\n";
  1.1597 +    }
  1.1598 +    let noteSpan = appendElementWithText(aP, "span", "mrNote", noteText);
  1.1599 +    noteSpan.title =
  1.1600 +      "This value is the sum of " + aNMerged +
  1.1601 +      " memory reports that all have the same path.";
  1.1602 +  }
  1.1603 +
  1.1604 +  if (aPresence) {
  1.1605 +    let c, title;
  1.1606 +    switch (aPresence) {
  1.1607 +     case DReport.PRESENT_IN_FIRST_ONLY:
  1.1608 +      c = '-';
  1.1609 +      title = "This value was only present in the first set of memory reports.";
  1.1610 +      break;
  1.1611 +     case DReport.PRESENT_IN_SECOND_ONLY:
  1.1612 +      c = '+';
  1.1613 +      title = "This value was only present in the second set of memory reports.";
  1.1614 +      break;
  1.1615 +     case DReport.ADDED_FOR_BALANCE:
  1.1616 +      c = '!';
  1.1617 +      title = "One of the sets of memory reports lacked children for this " +
  1.1618 +              "node's parent. This is a fake child node added to make the " +
  1.1619 +              "two memory sets comparable.";
  1.1620 +      break;
  1.1621 +     default: assert(false, "bad presence");
  1.1622 +      break;
  1.1623 +    }
  1.1624 +    let noteSpan = appendElementWithText(aP, "span", "mrNote",
  1.1625 +                                         " [" + c + "]\n");
  1.1626 +    noteSpan.title = title;
  1.1627 +  }
  1.1628 +}
  1.1629 +
  1.1630 +// This is used to record the (safe) IDs of which sub-trees have been manually
  1.1631 +// expanded (marked as true) and collapsed (marked as false).  It's used to
  1.1632 +// replicate the collapsed/expanded state when the page is updated.  It can end
  1.1633 +// up holding IDs of nodes that no longer exist, e.g. for compartments that
  1.1634 +// have been closed.  This doesn't seem like a big deal, because the number is
  1.1635 +// limited by the number of entries the user has changed from their original
  1.1636 +// state.
  1.1637 +let gShowSubtreesBySafeTreeId = {};
  1.1638 +
  1.1639 +function assertClassListContains(e, className) {
  1.1640 +  assert(e, "undefined " + className);
  1.1641 +  assert(e.classList.contains(className), "classname isn't " + className);
  1.1642 +}
  1.1643 +
  1.1644 +function toggle(aEvent)
  1.1645 +{
  1.1646 +  // This relies on each line being a span that contains at least four spans:
  1.1647 +  // mrValue, mrPerc, mrSep, mrName, and then zero or more mrNotes.  All
  1.1648 +  // whitespace must be within one of these spans for this function to find the
  1.1649 +  // right nodes.  And the span containing the children of this line must
  1.1650 +  // immediately follow.  Assertions check this.
  1.1651 +
  1.1652 +  // |aEvent.target| will be one of the spans.  Get the outer span.
  1.1653 +  let outerSpan = aEvent.target.parentNode;
  1.1654 +  assertClassListContains(outerSpan, "hasKids");
  1.1655 +
  1.1656 +  // Toggle the '++'/'--' separator.
  1.1657 +  let isExpansion;
  1.1658 +  let sepSpan = outerSpan.childNodes[2];
  1.1659 +  assertClassListContains(sepSpan, "mrSep");
  1.1660 +  if (sepSpan.textContent === kHideKidsSep) {
  1.1661 +    isExpansion = true;
  1.1662 +    sepSpan.textContent = kShowKidsSep;
  1.1663 +  } else if (sepSpan.textContent === kShowKidsSep) {
  1.1664 +    isExpansion = false;
  1.1665 +    sepSpan.textContent = kHideKidsSep;
  1.1666 +  } else {
  1.1667 +    assert(false, "bad sepSpan textContent");
  1.1668 +  }
  1.1669 +
  1.1670 +  // Toggle visibility of the span containing this node's children.
  1.1671 +  let subTreeSpan = outerSpan.nextSibling;
  1.1672 +  assertClassListContains(subTreeSpan, "kids");
  1.1673 +  subTreeSpan.classList.toggle("hidden");
  1.1674 +
  1.1675 +  // Record/unrecord that this sub-tree was toggled.
  1.1676 +  let safeTreeId = outerSpan.id;
  1.1677 +  if (gShowSubtreesBySafeTreeId[safeTreeId] !== undefined) {
  1.1678 +    delete gShowSubtreesBySafeTreeId[safeTreeId];
  1.1679 +  } else {
  1.1680 +    gShowSubtreesBySafeTreeId[safeTreeId] = isExpansion;
  1.1681 +  }
  1.1682 +}
  1.1683 +
  1.1684 +function expandPathToThisElement(aElement)
  1.1685 +{
  1.1686 +  if (aElement.classList.contains("kids")) {
  1.1687 +    // Unhide the kids.
  1.1688 +    aElement.classList.remove("hidden");
  1.1689 +    expandPathToThisElement(aElement.previousSibling);  // hasKids
  1.1690 +
  1.1691 +  } else if (aElement.classList.contains("hasKids")) {
  1.1692 +    // Change the separator to '--'.
  1.1693 +    let sepSpan = aElement.childNodes[2];
  1.1694 +    assertClassListContains(sepSpan, "mrSep");
  1.1695 +    sepSpan.textContent = kShowKidsSep;
  1.1696 +    expandPathToThisElement(aElement.parentNode);       // kids or pre.entries
  1.1697 +
  1.1698 +  } else {
  1.1699 +    assertClassListContains(aElement, "entries");
  1.1700 +  }
  1.1701 +}
  1.1702 +
  1.1703 +/**
  1.1704 + * Appends the elements for the tree, including its heading.
  1.1705 + *
  1.1706 + * @param aP
  1.1707 + *        The parent DOM node.
  1.1708 + * @param aRoot
  1.1709 + *        The tree root.
  1.1710 + * @param aProcess
  1.1711 + *        The process the tree corresponds to.
  1.1712 + * @param aPadText
  1.1713 + *        A string to pad the start of each entry.
  1.1714 + */
  1.1715 +function appendTreeElements(aP, aRoot, aProcess, aPadText)
  1.1716 +{
  1.1717 +  /**
  1.1718 +   * Appends the elements for a particular tree, without a heading.
  1.1719 +   *
  1.1720 +   * @param aP
  1.1721 +   *        The parent DOM node.
  1.1722 +   * @param aProcess
  1.1723 +   *        The process the tree corresponds to.
  1.1724 +   * @param aUnsafeNames
  1.1725 +   *        An array of the names forming the path to aT.
  1.1726 +   * @param aRoot
  1.1727 +   *        The root of the tree this sub-tree belongs to.
  1.1728 +   * @param aT
  1.1729 +   *        The tree.
  1.1730 +   * @param aTreelineText1
  1.1731 +   *        The first part of the treeline for this entry and this entry's
  1.1732 +   *        children.
  1.1733 +   * @param aTreelineText2a
  1.1734 +   *        The second part of the treeline for this entry.
  1.1735 +   * @param aTreelineText2b
  1.1736 +   *        The second part of the treeline for this entry's children.
  1.1737 +   * @param aParentStringLength
  1.1738 +   *        The length of the formatted byte count of the top node in the tree.
  1.1739 +   */
  1.1740 +  function appendTreeElements2(aP, aProcess, aUnsafeNames, aRoot, aT,
  1.1741 +                               aTreelineText1, aTreelineText2a,
  1.1742 +                               aTreelineText2b, aParentStringLength)
  1.1743 +  {
  1.1744 +    function appendN(aS, aC, aN)
  1.1745 +    {
  1.1746 +      for (let i = 0; i < aN; i++) {
  1.1747 +        aS += aC;
  1.1748 +      }
  1.1749 +      return aS;
  1.1750 +    }
  1.1751 +
  1.1752 +    // The tree line.  Indent more if this entry is narrower than its parent.
  1.1753 +    let valueText = aT.toString();
  1.1754 +    let extraTreelineLength =
  1.1755 +      Math.max(aParentStringLength - valueText.length, 0);
  1.1756 +    if (extraTreelineLength > 0) {
  1.1757 +      aTreelineText2a =
  1.1758 +        appendN(aTreelineText2a, kHorizontal, extraTreelineLength);
  1.1759 +      aTreelineText2b =
  1.1760 +        appendN(aTreelineText2b, " ",         extraTreelineLength);
  1.1761 +    }
  1.1762 +    let treelineText = aTreelineText1 + aTreelineText2a;
  1.1763 +    appendElementWithText(aP, "span", "treeline", treelineText);
  1.1764 +
  1.1765 +    // Detect and record invalid values.  But not if gIsDiff is true, because
  1.1766 +    // we expect negative values in that case.
  1.1767 +    assertInput(aRoot._units === aT._units,
  1.1768 +                "units within a tree are inconsistent");
  1.1769 +    let tIsInvalid = false;
  1.1770 +    if (!gIsDiff && !(0 <= aT._amount && aT._amount <= aRoot._amount)) {
  1.1771 +      tIsInvalid = true;
  1.1772 +      let unsafePath = aUnsafeNames.join("/");
  1.1773 +      gUnsafePathsWithInvalidValuesForThisProcess.push(unsafePath);
  1.1774 +      reportAssertionFailure("Invalid value (" + aT._amount + " / " +
  1.1775 +                             aRoot._amount + ") for " +
  1.1776 +                             flipBackslashes(unsafePath));
  1.1777 +    }
  1.1778 +
  1.1779 +    // For non-leaf nodes, the entire sub-tree is put within a span so it can
  1.1780 +    // be collapsed if the node is clicked on.
  1.1781 +    let d;
  1.1782 +    let sep;
  1.1783 +    let showSubtrees;
  1.1784 +    if (aT._kids) {
  1.1785 +      // Determine if we should show the sub-tree below this entry;  this
  1.1786 +      // involves reinstating any previous toggling of the sub-tree.
  1.1787 +      let unsafePath = aUnsafeNames.join("/");
  1.1788 +      let safeTreeId = aProcess + ":" + flipBackslashes(unsafePath);
  1.1789 +      showSubtrees = !aT._hideKids;
  1.1790 +      if (gShowSubtreesBySafeTreeId[safeTreeId] !== undefined) {
  1.1791 +        showSubtrees = gShowSubtreesBySafeTreeId[safeTreeId];
  1.1792 +      }
  1.1793 +      d = appendElement(aP, "span", "hasKids");
  1.1794 +      d.id = safeTreeId;
  1.1795 +      d.onclick = toggle;
  1.1796 +      sep = showSubtrees ? kShowKidsSep : kHideKidsSep;
  1.1797 +    } else {
  1.1798 +      assert(!aT._hideKids, "leaf node with _hideKids set")
  1.1799 +      sep = kNoKidsSep;
  1.1800 +      d = aP;
  1.1801 +    }
  1.1802 +
  1.1803 +    // The value.
  1.1804 +    appendElementWithText(d, "span", "mrValue" + (tIsInvalid ? " invalid" : ""),
  1.1805 +                          valueText);
  1.1806 +
  1.1807 +    // The percentage (omitted for single entries).
  1.1808 +    let percText;
  1.1809 +    if (!aT._isDegenerate) {
  1.1810 +      // Treat 0 / 0 as 100%.
  1.1811 +      let num = aRoot._amount === 0 ? 100 : (100 * aT._amount / aRoot._amount);
  1.1812 +      let numText = num.toFixed(2);
  1.1813 +      percText = numText === "100.00"
  1.1814 +               ? " (100.0%)"
  1.1815 +               : (0 <= num && num < 10 ? " (0" : " (") + numText + "%)";
  1.1816 +      appendElementWithText(d, "span", "mrPerc", percText);
  1.1817 +    }
  1.1818 +
  1.1819 +    // The separator.
  1.1820 +    appendElementWithText(d, "span", "mrSep", sep);
  1.1821 +
  1.1822 +    // The entry's name.
  1.1823 +    appendMrNameSpan(d, aT._description, aT._unsafeName,
  1.1824 +                     tIsInvalid, aT._nMerged, aT._presence);
  1.1825 +
  1.1826 +    // In non-verbose mode, invalid nodes can be hidden in collapsed sub-trees.
  1.1827 +    // But it's good to always see them, so force this.
  1.1828 +    if (!gVerbose.checked && tIsInvalid) {
  1.1829 +      expandPathToThisElement(d);
  1.1830 +    }
  1.1831 +
  1.1832 +    // Recurse over children.
  1.1833 +    if (aT._kids) {
  1.1834 +      // The 'kids' class is just used for sanity checking in toggle().
  1.1835 +      d = appendElement(aP, "span", showSubtrees ? "kids" : "kids hidden");
  1.1836 +
  1.1837 +      let kidTreelineText1 = aTreelineText1 + aTreelineText2b;
  1.1838 +      for (let i = 0; i < aT._kids.length; i++) {
  1.1839 +        let kidTreelineText2a, kidTreelineText2b;
  1.1840 +        if (i < aT._kids.length - 1) {
  1.1841 +          kidTreelineText2a = kVerticalAndRight_Right_Right;
  1.1842 +          kidTreelineText2b = kVertical_Space_Space;
  1.1843 +        } else {
  1.1844 +          kidTreelineText2a = kUpAndRight_Right_Right;
  1.1845 +          kidTreelineText2b = "   ";
  1.1846 +        }
  1.1847 +        aUnsafeNames.push(aT._kids[i]._unsafeName);
  1.1848 +        appendTreeElements2(d, aProcess, aUnsafeNames, aRoot, aT._kids[i],
  1.1849 +                            kidTreelineText1, kidTreelineText2a,
  1.1850 +                            kidTreelineText2b, valueText.length);
  1.1851 +        aUnsafeNames.pop();
  1.1852 +      }
  1.1853 +    }
  1.1854 +  }
  1.1855 +
  1.1856 +  let rootStringLength = aRoot.toString().length;
  1.1857 +  appendTreeElements2(aP, aProcess, [aRoot._unsafeName], aRoot, aRoot,
  1.1858 +                      aPadText, "", "", rootStringLength);
  1.1859 +}
  1.1860 +
  1.1861 +//---------------------------------------------------------------------------
  1.1862 +
  1.1863 +function appendSectionHeader(aP, aText)
  1.1864 +{
  1.1865 +  appendElementWithText(aP, "h2", "", aText + "\n");
  1.1866 +  return appendElement(aP, "pre", "entries");
  1.1867 +}
  1.1868 +
  1.1869 +//---------------------------------------------------------------------------
  1.1870 +
  1.1871 +function saveReportsToFile()
  1.1872 +{
  1.1873 +  let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
  1.1874 +  fp.appendFilter("Zipped JSON files", "*.json.gz");
  1.1875 +  fp.appendFilters(Ci.nsIFilePicker.filterAll);
  1.1876 +  fp.filterIndex = 0;
  1.1877 +  fp.addToRecentDocs = true;
  1.1878 +  fp.defaultString = "memory-report.json.gz";
  1.1879 +
  1.1880 +  let fpFinish = function(file) {
  1.1881 +    let dumper = Cc["@mozilla.org/memory-info-dumper;1"]
  1.1882 +                   .getService(Ci.nsIMemoryInfoDumper);
  1.1883 +
  1.1884 +    let finishDumping = () => {
  1.1885 +      updateMainAndFooter("Saved reports to " + file.path, HIDE_FOOTER);
  1.1886 +    }
  1.1887 +
  1.1888 +    dumper.dumpMemoryReportsToNamedFile(file.path, finishDumping, null);
  1.1889 +  }
  1.1890 +
  1.1891 +  let fpCallback = function(aResult) {
  1.1892 +    if (aResult == Ci.nsIFilePicker.returnOK ||
  1.1893 +        aResult == Ci.nsIFilePicker.returnReplace) {
  1.1894 +      fpFinish(fp.file);
  1.1895 +    }
  1.1896 +  };
  1.1897 +
  1.1898 +  try {
  1.1899 +    fp.init(window, "Save Memory Reports", Ci.nsIFilePicker.modeSave);
  1.1900 +  } catch(ex) {
  1.1901 +    // This will fail on Android, since there is no Save as file picker there.
  1.1902 +    // Just save to the default downloads dir if it does.
  1.1903 +    let file = Services.dirsvc.get("DfltDwnld", Ci.nsIFile);
  1.1904 +    file.append(fp.defaultString);
  1.1905 +    fpFinish(file);
  1.1906 +    return;
  1.1907 +  }
  1.1908 +  fp.open(fpCallback);
  1.1909 +}

mercurial