Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  aboutMemory.js   Sprache: JAVA

 
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-*/
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */


// You can direct about:memory to immediately load memory reports from a file
// by providing a file= query string.  For example,
//
//     about:memory?file=/home/username/reports.json.gz
//
// "file=" is not case-sensitive.  We'll URI-unescape the contents of the
// "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;

const KIND_NONHEAP = Ci.nsIMemoryReporter.KIND_NONHEAP;
const KIND_HEAP = Ci.nsIMemoryReporter.KIND_HEAP;
const KIND_OTHER = Ci.nsIMemoryReporter.KIND_OTHER;

const UNITS_BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
const UNITS_COUNT = Ci.nsIMemoryReporter.UNITS_COUNT;
const UNITS_COUNT_CUMULATIVE = Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE;
const UNITS_PERCENTAGE = Ci.nsIMemoryReporter.UNITS_PERCENTAGE;

const { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs"
);
const { NetUtil } = ChromeUtils.importESModule(
  "resource://gre/modules/NetUtil.sys.mjs"
);
ChromeUtils.defineESModuleGetters(this, {
  Downloads: "resource://gre/modules/Downloads.sys.mjs",
  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
});

ChromeUtils.defineLazyGetter(this"nsBinaryStream", () =>
  CC(
    "@mozilla.org/binaryinputstream;1",
    "nsIBinaryInputStream",
    "setInputStream"
  )
);
ChromeUtils.defineLazyGetter(this"nsFile", () =>
  CC("@mozilla.org/file/local;1""nsIFile""initWithPath")
);
ChromeUtils.defineLazyGetter(this"nsGzipConverter", () =>
  CC(
    "@mozilla.org/streamconv;1?from=gzip&to=uncompressed",
    "nsIStreamConverter"
  )
);

let gMgr = Cc["@mozilla.org/memory-reporter-manager;1"].getService(
  Ci.nsIMemoryReporterManager
);

const gPageName = "about:memory";
document.title = gPageName;

const gMainProcessPrefix = "Main Process";

const gFilterUpdateDelayMS = 300;

let gIsDiff = false;

let gCurrentReports = [];
let gCurrentHasMozMallocUsableSize = false;
let gCurrentIsDiff = false;

let gFilter = "";

// ---------------------------------------------------------------------------

// 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.
  return !aUnsafeStr.includes("\\")
    ? aUnsafeStr
    : aUnsafeStr.replace(/\\/g, "/");
}

const gAssertionFailureMsgPrefix = "aboutMemory.js assertion failed: ";

// This is used for things that should never fail, and indicate a defect in
// this file if they do.
function assert(aCond, aMsg) {
  if (!aCond) {
    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}`);
  }
}

function handleException(aEx) {
  let str = "" + aEx;
  if (str.startsWith(gAssertionFailureMsgPrefix)) {
    // Argh, assertion failure within this file!  Give up.
    throw aEx;
  } 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) {
  let section = appendElement(document.body, "div""section");
  appendElementWithText(section, "div""debug", JSON.stringify(aVal));
}

function stringMatchesFilter(aString, aFilter) {
  assert(
    typeof aFilter == "string" || aFilter instanceof RegExp,
    "unexpected aFilter type"
  );

  return typeof aFilter == "string"
    ? aString.includes(aFilter)
    : aFilter.test(aString);
}

// ---------------------------------------------------------------------------

window.onunload = function () {};

// ---------------------------------------------------------------------------

// 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;

// The <div> holding the footer.
let gFooter;

// The "verbose" checkbox.
let gVerbose;

// The "anonymize" checkbox.
let 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})`;
  }

  // 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");
  gMain.classList.remove("verbose");
  gMain.classList.remove("non-verbose");
  if (gVerbose) {
    gMain.classList.add(gVerbose.checked ? "verbose" : "non-verbose");
  }

  let msgElement;
  if (aMsg) {
    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()})`;
    }
    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");
  }
  return msgElement;
}

function updateMainAndFooter(aMsg, aShowTimestamp, aFooterAction, aClassName) {
  return updateTitleMainAndFooter(
    "",
    aMsg,
    aShowTimestamp,
    aFooterAction,
    aClassName
  );
}

function appendTextNode(aP, aText) {
  let e = document.createTextNode(aText);
  aP.appendChild(e);
  return e;
}

function appendElement(aP, aTagName, aClassName) {
  let e = newElement(aTagName, aClassName);
  aP.appendChild(e);
  return e;
}

function appendElementWithText(aP, aTagName, aClassName, aText) {
  let e = appendElement(aP, aTagName, aClassName);
  // Setting textContent clobbers existing children, but there are none.  More
  // importantly, it avoids creating a JS-land object for the node, saving
  // memory.
  e.textContent = aText;
  return e;
}

function newElement(aTagName, aClassName) {
  let e = document.createElement(aTagName);
  if (aClassName) {
    e.className = aClassName;
  }
  return e;
}

// ---------------------------------------------------------------------------

const explicitTreeDescription =
  "This tree covers explicit memory allocations by the application. It includes \
\n\n\
* all allocations made at the heap allocation level (via functions such as malloc, \
calloc, realloc, memalign, operator new, and operator new[]) that have not been \
explicitly decommitted (i.e. evicted from memory and swap), and \
\n\n\
* some allocations (those covered by memory reporters) made at the operating \
system level (via calls to functions such as VirtualAlloc, vm_allocate, and \
mmap), \
\n\n\
* where possible, the overhead of the heap allocator itself.\
\n\n\
It excludes memory that is mapped implicitly such as code and data segments, \
and thread stacks. \
\n\n\
'explicit' is not guaranteed to cover every explicit allocation, but it does cover \
most (including the entire heap), and therefore it is the single best number to \
focus on when trying to reduce memory usage.";

// ---------------------------------------------------------------------------

function appendButton(aP, aTitle, aOnClick, aText, aId) {
  let b = appendElementWithText(aP, "button""", aText);
  b.title = aTitle;
  b.onclick = aOnClick;
  if (aId) {
    b.id = aId;
  }
  return b;
}

function appendHiddenFileInput(aP, aId, aChangeListener) {
  let input = appendElementWithText(aP, "input""hidden""");
  input.type = "file";
  input.id = aId; // used in testing
  input.addEventListener("change", aChangeListener);
  return input;
}

window.onload = function () {
  // Generate the header.

  let header = appendElement(document.body, "div""ancillary");

  // 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 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
        // onchange handler to be re-called without having to go via the file
        // picker.
        if (!aElem.skipClick) {
          this.click();
        }
      } else {
        let filename1 = this.filename1;
        delete this.filename1;
        updateAboutMemoryFromTwoFiles(filename1, file.mozFullPath);
      }
    }
  );

  const CuDesc = "Measure current memory reports and show.";
  const LdDesc = "Load memory reports from file and show.";
  const DfDesc =
    "Load memory report data from two files and show the difference.";

  const SvDesc = "Save memory reports to file.";

  const GCDesc = "Do a global garbage collection.";
  const CCDesc = "Do a cycle collection.";
  const MMDesc =
    'Send three "heap-minimize" notifications in a ' +
    "row. Each notification triggers a global garbage " +
    "collection followed by a cycle collection, and causes the " +
    "process to reduce memory usage in other ways, e.g. by " +
    "flushing various caches.";

  const GCAndCCLogDesc =
    "Save garbage collection log and concise cycle " +
    "collection log.\n" +
    "WARNING: These logs may be large (>1GB).";
  const GCAndCCAllLogDesc =
    "Save garbage collection log and verbose cycle " +
    "collection log.\n" +
    "WARNING: These logs may be large (>1GB).";

  const DMDEnabledDesc =
    "Analyze memory reports coverage and save the " +
    "output to the temp directory.\n";
  const DMDDisabledDesc =
    "DMD is not running. Please re-start with $DMD and " +
    "the other relevant environment variables set " +
    "appropriately.";

  let ops = appendElement(header, "div""");

  let row1 = appendElement(ops, "div""opsRow");

  let labelDiv1 = appendElementWithText(
    row1,
    "div",
    "opsRowLabel",
    "Show memory reports"
  );
  labelDiv1.setAttribute("role""heading");
  labelDiv1.setAttribute("aria-level""1");
  let label1 = appendElementWithText(labelDiv1, "label""");
  gVerbose = appendElement(label1, "input""");
  gVerbose.type = "checkbox";
  gVerbose.id = "verbose"// used for testing
  appendTextNode(label1, "verbose");

  // 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…");

  let row2 = appendElement(ops, "div""opsRow");

  let labelDiv2 = appendElementWithText(
    row2,
    "div",
    "opsRowLabel",
    "Save memory reports"
  );
  labelDiv2.setAttribute("role""heading");
  labelDiv2.setAttribute("aria-level""1");
  appendButton(row2, SvDesc, saveReportsToFile, "Measure and save…");

  // 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");

  let row3 = appendElement(ops, "div""opsRow");

  let labelDiv3 = appendElementWithText(
    row3,
    "div",
    "opsRowLabel",
    "Free memory"
  );
  labelDiv3.setAttribute("role""heading");
  labelDiv3.setAttribute("aria-level""1");
  appendButton(row3, GCDesc, doGC, "GC");
  appendButton(row3, CCDesc, doCC, "CC");
  appendButton(row3, MMDesc, doMMU, "Minimize memory usage");

  let row4 = appendElement(ops, "div""opsRow");

  let labelDiv4 = appendElementWithText(
    row4,
    "div",
    "opsRowLabel",
    "Save GC & CC logs"
  );
  labelDiv4.setAttribute("role""heading");
  labelDiv4.setAttribute("aria-level""1");
  appendButton(
    row4,
    GCAndCCLogDesc,
    saveGCLogAndConciseCCLog,
    "Save concise",
    "saveLogsConcise"
  );
  appendButton(
    row4,
    GCAndCCAllLogDesc,
    saveGCLogAndVerboseCCLog,
    "Save verbose",
    "saveLogsVerbose"
  );

  // Three cases here:
  // - DMD is disabled (i.e. not built): don't show the button.
  // - DMD is enabled but is not running: show the button, but disable it.
  // - DMD is enabled and is running: show the button and enable it.
  if (gMgr.isDMDEnabled) {
    let row5 = appendElement(ops, "div""opsRow");

    let labelDiv5 = appendElementWithText(
      row5,
      "div",
      "opsRowLabel",
      "Save DMD output"
    );
    labelDiv5.setAttribute("role""heading");
    labelDiv5.setAttribute("aria-level""1");
    let enableButtons = gMgr.isDMDRunning;

    let dmdButton = appendButton(
      row5,
      enableButtons ? DMDEnabledDesc : DMDDisabledDesc,
      doDMD,
      "Save"
    );
    dmdButton.disabled = !enableButtons;
  }

  // Generate the main div, where content ("section" divs) will go.  It's
  // hidden at first.

  gMain = appendElement(document.body, "div""");
  gMain.id = "mainDiv";

  // Generate the footer.  It's hidden at first.

  gFooter = appendElement(document.body, "div""ancillary hidden");
  gFooter.setAttribute("role""contentinfo");

  if (Services.policies.isAllowed("aboutSupport")) {
    let a = appendElementWithText(
      gFooter,
      "a",
      "option",
      "Troubleshooting information"
    );
    a.href = "about:support";
  }

  let legendText1 =
    "Click on a non-leaf node in a tree to expand ('++') " +
    "or collapse ('--') its children.";
  let legendText2 =
    "Hover the pointer over the name of a memory report " +
    "to see a description of what it measures.";

  appendElementWithText(gFooter, "div""legend", legendText1);
  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);
        updateAboutMemoryFromFile(decodeURIComponent(filename));
        return;
      }
    }
  }
};

// ---------------------------------------------------------------------------

function doGC() {
  Services.obs.notifyObservers(null"child-gc-request");
  Cu.forceGC();
  updateMainAndFooter(
    "Garbage collection completed",
    SHOW_TIMESTAMP,
    HIDE_FOOTER
  );
}

function doCC() {
  Services.obs.notifyObservers(null"child-cc-request");
  window.windowUtils.cycleCollect();
  updateMainAndFooter(
    "Cycle collection completed",
    SHOW_TIMESTAMP,
    HIDE_FOOTER
  );
}

function doMMU() {
  Services.obs.notifyObservers(null"child-mmu-request");
  gMgr.minimizeMemoryUsage(() =>
    updateMainAndFooter(
      "Memory minimization completed",
      SHOW_TIMESTAMP,
      HIDE_FOOTER
    )
  );
}

function doMeasure() {
  updateAboutMemoryFromReporters();
}

function saveGCLogAndConciseCCLog() {
  dumpGCLogAndCCLog(false);
}

function saveGCLogAndVerboseCCLog() {
  dumpGCLogAndCCLog(true);
}

function doDMD() {
  updateMainAndFooter(
    "Saving memory reports and DMD output...",
    NO_TIMESTAMP,
    HIDE_FOOTER
  );
  try {
    let dumper = Cc["@mozilla.org/memory-info-dumper;1"].getService(
      Ci.nsIMemoryInfoDumper
    );

    dumper.dumpMemoryInfoToTempDir(
      /* identifier = */ "",
      gAnonymize.checked,
      /* minimize = */ false
    );
    updateMainAndFooter(
      "Saved memory reports and DMD reports analysis " +
        "to the temp directory",
      SHOW_TIMESTAMP,
      HIDE_FOOTER
    );
  } catch (ex) {
    updateMainAndFooter(ex.toString(), NO_TIMESTAMP, HIDE_FOOTER);
  }
}

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) {
    appendElementWithText(section, "div""""Saved GC log to " + aGCLog.path);

    let ccLogType = aVerbose ? "verbose" : "concise";
    appendElementWithText(
      section,
      "div",
      "",
      "Saved " + ccLogType + " CC log to " + aCCLog.path
    );
  }

  dumper.dumpGCAndCCLogsToFile("", aVerbose, /* dumpChildProcesses = */ true, {
    onDump: displayInfo,
    onFinish() {
      inProgress.remove();
    },
  });
}

/**
 * Top-level function that does the work of generating the page from the memory
 * reporters.
 */

function updateAboutMemoryFromReporters() {
  updateMainAndFooter("Measuring...", NO_TIMESTAMP, HIDE_FOOTER);

  try {
    gCurrentReports = [];
    gCurrentHasMozMallocUsableSize = gMgr.hasMozMallocUsableSize;
    gCurrentIsDiff = false;
    gFilter = "";

    // Record the reports from the live memory reporters then process them.
    let handleReport = function (
      aProcess,
      aUnsafePath,
      aKind,
      aUnits,
      aAmount,
      aDescription
    ) {
      gCurrentReports.push({
        process: aProcess,
        path: aUnsafePath,
        kind: aKind,
        units: aUnits,
        amount: aAmount,
        description: aDescription,
      });
    };

    let displayReports = function () {
      updateTitleMainAndFooter(
        "live measurement",
        "",
        NO_TIMESTAMP,
        SHOW_FOOTER
      );
      updateAboutMemoryFromCurrentData();
    };

    gMgr.getReports(
      handleReport,
      null,
      displayReports,
      null,
      gAnonymize.checked
    );
  } catch (ex) {
    handleException(ex);
  }
}

// Increment this if the JSON format changes.
//
let 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.
 * @return The extracted object.
 */

function parseAndUnwrapIfCrashDump(aStr) {
  let obj = JSON.parse(aStr);
  if (obj.memory_report !== undefined) {
    // It looks like a crash dump. The memory reports should be in the
    // |memory_report| property.
    obj = obj.memory_report;
  }
  return obj;
}

/**
 * Populate about:memory using the data stored in gCurrentReports and
 * gCurrentHasMozMallocUsableSize.
 */

function updateAboutMemoryFromCurrentData() {
  function processCurrentMemoryReports(aHandleReport, aDisplayReports) {
    for (let r of gCurrentReports) {
      aHandleReport(
        r.process,
        r.path,
        r.kind,
        r.units,
        r.amount,
        r.description,
        r._presence
      );
    }
    aDisplayReports();
  }

  gIsDiff = gCurrentIsDiff;
  appendAboutMemoryMain(
    processCurrentMemoryReports,
    gFilter,
    gCurrentHasMozMallocUsableSize
  );
  gIsDiff = false;
}

/**
 * Populate about:memory using the data in the given JSON object.
 *
 * @param aObj
 *        An object that (hopefully!) conforms to the JSON schema used by
 *        nsIMemoryInfoDumper.
 */

function updateAboutMemoryFromJSONObject(aObj) {
  try {
    assertInput(
      aObj.version === gCurrentFileFormatVersion,
      "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"
    );

    gCurrentReports = aObj.reports.concat();
    gCurrentHasMozMallocUsableSize = aObj.hasMozMallocUsableSize;
    gCurrentIsDiff = gIsDiff;
    gFilter = "";

    updateAboutMemoryFromCurrentData();
  } catch (ex) {
    handleException(ex);
  }
}

/**
 * Populate about:memory using the data in the given JSON string.
 *
 * @param aStr
 *        A string containing JSON data conforming to the schema used by
 *        nsIMemoryReporterManager::dumpReports.
 */

function updateAboutMemoryFromJSONString(aStr) {
  try {
    let obj = parseAndUnwrapIfCrashDump(aStr);
    updateAboutMemoryFromJSONObject(obj);
  } catch (ex) {
    handleException(ex);
  }
}

/**
 * Loads the contents of a file into a string and passes that to a callback.
 *
 * @param aFilename
 *        The name of the file being read from.
 * @param aTitleNote
 *        A description to put in the page title upon completion.
 * @param aFn
 *        The function to call and pass the read string to upon completion.
 */

function loadMemoryReportsFromFile(aFilename, aTitleNote, aFn) {
  updateMainAndFooter("Loading...", NO_TIMESTAMP, HIDE_FOOTER);

  try {
    let reader = new FileReader();
    reader.onerror = () => {
      throw new Error("FileReader.onerror");
    };
    reader.onabort = () => {
      throw new Error("FileReader.onabort");
    };
    reader.onload = aEvent => {
      // Clear "Loading..." from above.
      updateTitleMainAndFooter(aTitleNote, "", NO_TIMESTAMP, SHOW_FOOTER);
      aFn(aEvent.target.result);
    };

    // If it doesn't have a .gz suffix, read it as a (legacy) ungzipped file.
    if (!aFilename.endsWith(".gz")) {
      File.createFromFileName(aFilename).then(file => {
        reader.readAsText(file);
      });
      return;
    }

    // Read compressed gzip file.
    let converter = new nsGzipConverter();
    converter.asyncConvertData(
      "gzip",
      "uncompressed",
      {
        data: [],
        onStartRequest() {},
        onDataAvailable(aR, aStream, aO, aCount) {
          let bi = new nsBinaryStream(aStream);
          this.data.push(bi.readBytes(aCount));
        },
        onStopRequest(aR, aC, aStatusCode) {
          try {
            if (!Components.isSuccessCode(aStatusCode)) {
              throw new Components.Exception(
                "Error while reading gzip file",
                aStatusCode
              );
            }
            reader.readAsText(new Blob(this.data));
          } catch (ex) {
            handleException(ex);
          }
        },
      },
      null
    );

    let file = new nsFile(aFilename);
    let fileChan = NetUtil.newChannel({
      uri: Services.io.newFileURI(file),
      loadUsingSystemPrincipal: true,
    });
    fileChan.asyncOpen(converter);
  } catch (ex) {
    handleException(ex);
  }
}

/**
 * Like updateAboutMemoryFromReporters(), but gets its data from a file instead
 * of the memory reporters.
 *
 * @param aFilename
 *        The name of the file being read from.  The expected format of the
 *        file's contents is described in a comment in nsIMemoryInfoDumper.idl.
 */

function updateAboutMemoryFromFile(aFilename) {
  loadMemoryReportsFromFile(
    aFilename,
    /* title note */ aFilename,
    updateAboutMemoryFromJSONString
  );
}

/**
 * Like updateAboutMemoryFromFile(), but gets its data from a two files and
 * 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}`;
  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;
      } catch (ex) {
        handleException(ex);
      }
    });
  });
}

// ---------------------------------------------------------------------------

// Something unlikely to appear in a process name.
let 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;
  if (aPresence !== undefined) {
    this._presence = aPresence;
  }
}

DReport.prototype = {
  assertCompatible(aKind, aUnits) {
    assert(this._kind == aKind, "Mismatched kinds");
    assert(this._units == aUnits, "Mismatched units");

    // We don't check that the "description" properties match.  This is because
    // on Linux we can get cases where the paths are the same but the
    // descriptions differ, like this:
    //
    //   "path": "size/other-files/icon-theme.cache/[r--p]",
    //   "description": "/usr/share/icons/gnome/icon-theme.cache (read-only, not executable, private)"
    //
    //   "path": "size/other-files/icon-theme.cache/[r--p]"
    //   "description": "/usr/share/icons/hicolor/icon-theme.cache (read-only, not executable, private)"
    //
    // In those cases, we just use the description from the first-encountered
    // one, which is what about:memory also does.
    // (Note: reports with those paths are no longer generated, but allowing
    // the descriptions to differ seems reasonable.)
  },

  merge(aJr) {
    this.assertCompatible(aJr.kind, aJr.units);
    this._amount += aJr.amount;
    this._nMerged++;
  },

  toJSON(aProcess, aPath, aAmount) {
    return {
      process: aProcess,
      path: aPath,
      kind: this._kind,
      units: this._units,
      amount: aAmount,
      description: this._description,
      _presence: this._presence,
    };
  },
};

// Constants that indicate if a DReport was present only in one of the data
// sets, or had to be added for balance.
DReport.PRESENT_IN_FIRST_ONLY = 1;
DReport.PRESENT_IN_SECOND_ONLY = 2;
DReport.ADDED_FOR_BALANCE = 3;

/**
 * Return true if the report contains a webIsolated process,
 * which is a good indication that Fission is enabled.
 */

function hasWebIsolatedProcess(aJSONReports) {
  for (let jr of aJSONReports) {
    assert(jr.process !== undefined, "Missing process");
    if (jr.process.startsWith("webIsolated")) {
      return true;
    }
  }
  return false;
}

/**
 * Make a report map, which has combined path+process strings for keys, and
 * DReport objects for values.
 *
 * @param aJSONReports
 *        The |reports| field of a JSON object.
 * @param aForgetIsolation
          If this is true, treat webIsolated processes like web processes.
 * @return The constructed report map.
 */

function makeDReportMap(aJSONReports, aForgetIsolation) {
  let dreportMap = {};
  for (let jr of aJSONReports) {
    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.
    // Ideally the memory reports themselves would contain information about
    // which parts of the the process and path need to be stripped -- saving us
    // from hardwiring knowledge of specific reporters here -- but we have no
    // mechanism for that. (Any future redesign of how memory reporters work
    // should include such a mechanism.)

    // Strip PIDs:
    // - pid 123
    // - pid=123
    // - pid: 123
    let pidRegex = /pid([ =]|: )\d+/g;
    let pidSubst = "pid$1NNN";
    let process = jr.process.replace(pidRegex, pidSubst);
    let path = jr.path.replace(pidRegex, pidSubst);

    if (aForgetIsolation && process.startsWith("webIsolated")) {
      process = "web (pid NNN)";
    }

    // Strip TIDs and threadpool IDs.
    path = path.replace(/\(tid=(\d+)\)/, "(tid=NNN)");
    path = path.replace(/#\d+ \(tid=NNN\)/, "#N (tid=NNN)");

    // Strip addresses:
    // - .../js-zone(0x12345678)/...
    // - .../zone(0x12345678)/...
    // - .../worker(<URL>, 0x12345678)/...
    path = path.replace(/zone\(0x[0-9A-Fa-f]+\)\//, "zone(0xNNN)/");
    path = path.replace(
      /\/worker\((.+), 0x[0-9A-Fa-f]+\)\//,
      "/worker($1, 0xNNN)/"
    );

    // Strip top window IDs:
    // - explicit/window-objects/top(<URL>, id=123)/...
    // - event-counts/window-objects/top(<URL>, id=123)/...
    path = path.replace(
      /^((?:explicit|event-counts)\/window-objects\/top\(.*, id=)\d+\)/,
      "$1NNN)"
    );

    // Strip null principal UUIDs (but not other UUIDs, because they may be
    // deterministic, such as those used by add-ons).
    path = path.replace(
      /moz-nullprincipal:{........-....-....-....-............}/g,
      "moz-nullprincipal:{NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN}"
    );

    // Strip segment counts from address-space.
    if (path.startsWith("address-space")) {
      path = path.replace(/\(segments=\d+\)/g, "(segments=NNNN)");
    }

    // Normalize omni.ja! paths.
    path = path.replace(
      /jar:file:\\\\\\(.+)\\omni.ja!/,
      "jar:file:\\\\\\...\\omni.ja!"
    );

    // Normalize script source counts.
    path = path.replace(/source\(scripts=(\d+), /, "source(scripts=NNN, ");

    let processPath = process + kProcessPathSep + path;
    let rOld = dreportMap[processPath];
    if (rOld === undefined) {
      dreportMap[processPath] = new DReport(
        jr.kind,
        jr.units,
        jr.amount,
        jr.description,
        1,
        undefined
      );
    } else {
      rOld.merge(jr);
    }
  }
  return dreportMap;
}

// Return a new dreportMap which is the diff of two dreportMaps.  Empties
// aDReportMap2 along the way.
function diffDReportMaps(aDReportMap1, aDReportMap2) {
  let result = {};

  for (let processPath in aDReportMap1) {
    let r1 = aDReportMap1[processPath];
    let r2 = aDReportMap2[processPath];
    let r2_amount, r2_nMerged;
    let presence;
    if (r2 !== undefined) {
      r1.assertCompatible(r2._kind, r2._units);
      r2_amount = r2._amount;
      r2_nMerged = r2._nMerged;
      delete aDReportMap2[processPath];
      presence = undefined; // represents that it's present in both
    } else {
      r2_amount = 0;
      r2_nMerged = 0;
      presence = DReport.PRESENT_IN_FIRST_ONLY;
    }
    result[processPath] = new DReport(
      r1._kind,
      r1._units,
      r2_amount - r1._amount,
      r1._description,
      Math.max(r1._nMerged, r2_nMerged),
      presence
    );
  }

  for (let processPath in aDReportMap2) {
    let r2 = aDReportMap2[processPath];
    result[processPath] = new DReport(
      r2._kind,
      r2._units,
      r2._amount,
      r2._description,
      r2._nMerged,
      DReport.PRESENT_IN_SECOND_ONLY
    );
  }

  return result;
}

function makeJSONReports(aDReportMap) {
  let reports = [];
  for (let processPath in aDReportMap) {
    let r = aDReportMap[processPath];
    if (r._amount !== 0) {
      // If _nMerged > 1, we give the full (aggregated) amount in the first
      // copy, and then use amount=0 in the remainder.  When viewed in
      // about:memory, this shows up as an entry with a "[2]"-style suffix
      // and the correct amount.
      let split = processPath.split(kProcessPathSep);
      assert(split.length >= 2);
      let process = split.shift();
      let path = split.join();
      reports.push(r.toJSON(process, path, r._amount));
      for (let i = 1; i < r._nMerged; i++) {
        reports.push(r.toJSON(process, path, 0));
      }
    }
  }

  return reports;
}

// Diff two JSON objects holding memory reports.
function diffJSONObjects(aJson1, aJson2) {
  function simpleProp(aProp) {
    assert(
      aJson1[aProp] !== undefined && aJson1[aProp] === aJson2[aProp],
      aProp + " properties don't match"
    );
    return aJson1[aProp];
  }

  // If one report we're diffing contains webIsolated processes, but the other
  // does not, then we're probably comparing a report with Fission enabled with
  // one where it is not enabled. In this case, we want to make all of the
  // webIsolated processes look like plain old web processes to get a better
  // diff.
  let hasIsolated1 = hasWebIsolatedProcess(aJson1.reports);
  let hasIsolated2 = hasWebIsolatedProcess(aJson2.reports);
  let eitherIsolated = hasIsolated1 || hasIsolated2;
  let forgetIsolation = hasIsolated1 != hasIsolated2 && eitherIsolated;

  return {
    version: simpleProp("version"),

    hasMozMallocUsableSize: simpleProp("hasMozMallocUsableSize"),

    reports: makeJSONReports(
      diffDReportMaps(
        makeDReportMap(aJson1.reports, forgetIsolation),
        makeDReportMap(aJson2.reports, forgetIsolation)
      )
    ),
  };
}

// ---------------------------------------------------------------------------

// |PColl| is short for "process collection".
function PColl() {
  this._trees = {};
  this._degenerates = {};
  this._heapTotal = 0;
}

/**
 * Processes reports (whether from reporters or from a file) and append the
 * main part of the page.
 *
 * @param aProcessReports
 *        Function that extracts the memory reports from the reporters or from
 *        file.
 * @param aFilter
 *        String or RegExp used to filter reports by their path.
 * @param aHasMozMallocUsableSize
 *        Boolean indicating if moz_malloc_usable_size works.
 */

function appendAboutMemoryMain(
  aProcessReports,
  aFilter,
  aHasMozMallocUsableSize
) {
  let pcollsByProcess = {};
  let infoByProcess = {};

  function handleReport(
    aProcess,
    aUnsafePath,
    aKind,
    aUnits,
    aAmount,
    aDescription,
    aPresence
  ) {
    if (aUnsafePath.startsWith("explicit/")) {
      assertInput(
        aKind === KIND_HEAP || aKind === KIND_NONHEAP,
        "bad explicit kind"
      );
      assertInput(aUnits === UNITS_BYTES, "bad explicit units");
    }

    assert(
      aPresence === undefined ||
        aPresence == DReport.PRESENT_IN_FIRST_ONLY ||
        aPresence == DReport.PRESENT_IN_SECOND_ONLY,
      "bad presence"
    );

    // If the process is empty, that means this process -- which is the main
    // process, because this is chrome JS code -- is doing the dumping.
    // Generate the process identifier: `Main Process (pid $PID)`.
    //
    // Note that `HandleReportAndFinishReportingCallbacks::Callback()` handles
    // this when saving memory reports to file. So, if we are loading memory
    // reports from file then `aProcess` will already be non-empty.
    let process = aProcess
      ? aProcess
      : gMainProcessPrefix + " (pid " + Services.appinfo.processID + ")";

    // Store the "resident" value for each process, so that if we filter it
    // out, we can still use it to correctly sort processes and generate the
    // process index.
    let info = infoByProcess[process];
    if (!info) {
      info = infoByProcess[process] = {};
    }
    if (aUnsafePath == "resident") {
      infoByProcess[process].resident = aAmount;
    }

    // Ignore reports that don't match the current filter.
    if (!stringMatchesFilter(aUnsafePath, aFilter)) {
      return;
    }

    let unsafeNames = aUnsafePath.split("/");
    let unsafeName0 = unsafeNames[0];
    let isDegenerate = unsafeNames.length === 1;

    // Get the PColl table for the process, creating it if necessary.
    let pcoll = pcollsByProcess[process];
    if (!pcollsByProcess[process]) {
      pcoll = pcollsByProcess[process] = new PColl();
    }

    // Get the root node, creating it if necessary.
    let psubcoll = isDegenerate ? pcoll._degenerates : pcoll._trees;
    let t = psubcoll[unsafeName0];
    if (!t) {
      t = psubcoll[unsafeName0] = new TreeNode(
        unsafeName0,
        aUnits,
        isDegenerate
      );
    }

    if (!isDegenerate) {
      // Add any missing nodes in the tree implied by aUnsafePath, and fill in
      // the properties that we can with a top-down traversal.
      for (let i = 1; i < unsafeNames.length; i++) {
        let unsafeName = unsafeNames[i];
        let u = t.findKid(unsafeName);
        if (!u) {
          u = new TreeNode(unsafeName, aUnits, isDegenerate);
          if (!t._kids) {
            t._kids = [];
          }
          t._kids.push(u);
        }
        t = u;
      }

      // Update the heap total if necessary.
      if (unsafeName0 === "explicit" && aKind == KIND_HEAP) {
        pcollsByProcess[process]._heapTotal += aAmount;
      }
    }

    if (t._amount) {
      // Duplicate!  Sum the values and mark it as a dup.
      t._amount += aAmount;
      t._nMerged = t._nMerged ? t._nMerged + 1 : 2;
      assert(t._presence === aPresence, "presence mismatch");
    } else {
      // New leaf node.  Fill in extra node details from the report.
      t._amount = aAmount;
      t._description = aDescription;
      if (aPresence !== undefined) {
        t._presence = aPresence;
      }
    }
  }

  function displayReports() {
    // Sort the processes.
    let processes = Object.keys(infoByProcess);
    processes.sort(function (aProcessA, aProcessB) {
      assert(
        aProcessA != aProcessB,
        `Elements of Object.keys() should be unique, but ` +
          `saw duplicate '${aProcessA}' elem.`
      );

      // Always put the main process first.
      if (aProcessA.startsWith(gMainProcessPrefix)) {
        return -1;
      }
      if (aProcessB.startsWith(gMainProcessPrefix)) {
        return 1;
      }

      // Then sort by resident size.
      let residentA = infoByProcess[aProcessA].resident || -1;
      let residentB = infoByProcess[aProcessB].resident || -1;
      if (residentA > residentB) {
        return -1;
      }
      if (residentA < residentB) {
        return 1;
      }

      // Then sort by process name.
      if (aProcessA < aProcessB) {
        return -1;
      }
      if (aProcessA > aProcessB) {
        return 1;
      }

      return 0;
    });

    // We set up this general layout inside gMain:
    //
    //   <div class="outputContainer">
    //     <div class="sections"></div>
    //     <div class="sidebar">
    //       <div class="sidebarContents">
    //         <div class="sidebarItem filterItem"></div>
    //         <div class="sidebarItem indexItem"></div>
    //       </div>
    //     </div>
    //   </div>
    //
    // If we detect that outputContainer already exists, then this is an update
    // (due to typing in a filter string) to an already-displayed memory report.
    // In this case we preserve the structure of the layout and only replace
    // div.sections and #indexItem. Preserving the filter sidebar item means we
    // preserve any editing state in its <input>.

    // Generate the main process sections.
    let sections = newElement("div""sections");
    sections.setAttribute("role""main");

    for (let [i, process] of processes.entries()) {
      let pcolls = pcollsByProcess[process];
      if (!pcolls) {
        continue;
      }

      let section = appendElement(sections, "div""section");
      appendProcessAboutMemoryElements(
        section,
        i,
        process,
        pcolls._trees,
        pcolls._degenerates,
        pcolls._heapTotal,
        aHasMozMallocUsableSize,
        aFilter != ""
      );
    }

    if (!sections.firstChild) {
      appendElementWithText(sections, "div""section""No results found.");
    }

    // Generate the process index.
    let indexItem = newElement("div""sidebarItem");
    indexItem.classList.add("indexItem");
    appendElementWithText(indexItem, "div""sidebarLabel""Process index");
    let indexList = appendElement(indexItem, "ul""index");

    for (let [i, process] of processes.entries()) {
      let indexListItem = appendElement(indexList, "li");
      let pcolls = pcollsByProcess[process];
      if (pcolls) {
        let indexLink = appendElementWithText(indexListItem, "a""", process);
        indexLink.href = "#start" + i;
      } else {
        // We've filtered out all reports from this process. Generate a non-link
        // entry in the process index, and skip creating a process report
        // section.
        indexListItem.textContent = process;
      }
    }

    // If we are updating, just swap in the new process output.
    let outputContainer = gMain.querySelector(".outputContainer");
    if (outputContainer) {
      outputContainer.querySelector(".sections").replaceWith(sections);
      outputContainer.querySelector(".indexItem").replaceWith(indexItem);
      return;
    }

    // Otherwise, generate the rest of the layout.
    outputContainer = appendElement(gMain, "div""outputContainer");
    outputContainer.appendChild(sections);

    let sidebar = appendElement(outputContainer, "div""sidebar");
    sidebar.setAttribute("role""navigation");
    let sidebarContents = appendElement(sidebar, "div""sidebarContents");

    // Generate the filter input and checkbox.
    let filterItem = appendElement(sidebarContents, "div""sidebarItem");
    filterItem.classList.add("filterItem");
    appendElementWithText(filterItem, "div""sidebarLabel""Filter");

    let filterInput = appendElement(filterItem, "input""filterInput");
    filterInput.placeholder = "Memory report path filter";
    filterInput.setAttribute("type""text");

    let filterOptions = appendElement(filterItem, "div");
    let filterRegExLabel = appendElement(filterOptions, "label");
    let filterRegExCheckbox = appendElement(filterRegExLabel, "input");
    filterRegExCheckbox.type = "checkbox";
    filterRegExLabel.append(" Regular expression");

    // Set up event handlers to update the display if the filter input or
    // checkbox changes.
    let filterUpdateTimeout;
    let filterUpdate = function () {
      if (filterUpdateTimeout) {
        window.clearTimeout(filterUpdateTimeout);
      }
      filterUpdateTimeout = window.setTimeout(function () {
        try {
          gFilter =
            filterRegExCheckbox.checked && filterInput.value != ""
              ? new RegExp(filterInput.value)
              : filterInput.value;
        } catch (ex) {
          // Match nothing if the regex was invalid.
          gFilter = new RegExp("^$");
        }
        updateAboutMemoryFromCurrentData();
      }, gFilterUpdateDelayMS);
    };
    filterInput.oninput = filterUpdate;
    filterRegExCheckbox.onchange = filterUpdate;

    // Append the process list item after the filter item.
    sidebarContents.appendChild(indexItem);
  }

  aProcessReports(handleReport, displayReports);
}

// ---------------------------------------------------------------------------

// There are two kinds of TreeNode.
// - Leaf TreeNodes correspond to reports.
// - Non-leaf TreeNodes are just scaffolding nodes for the tree;  their values
//   are derived from their children.
// Some trees are "degenerate", i.e. they contain a single node, i.e. they
// correspond to a report whose path has no '/' separators.
function TreeNode(aUnsafeName, aUnits, aIsDegenerate) {
  this._units = aUnits;
  this._unsafeName = aUnsafeName;
  if (aIsDegenerate) {
    this._isDegenerate = true;
  }

  // Leaf TreeNodes have these properties added immediately after construction:
  // - _amount
  // - _description
  // - _nMerged (only defined if > 1)
  // - _presence (only defined if value is PRESENT_IN_{FIRST,SECOND}_ONLY)
  //
  // Non-leaf TreeNodes have these properties added later:
  // - _kids
  // - _amount
  // - _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;
        }
      }
    }
    return undefined;
  },

  // When gIsDiff is false, tree operations -- sorting and determining if a
  // sub-tree is significant -- are straightforward. But when gIsDiff is true,
  // the combination of positive and negative values within a tree complicates
  // things. So for a non-leaf node, instead of just looking at _amount, we
  // instead look at the maximum absolute value of the node and all of its
  // descendants.
  maxAbsDescendant() {
    if (!this._kids) {
      // No kids? Just return the absolute value of the amount.
      return Math.abs(this._amount);
    }

    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());
    }
    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_PERCENTAGE:
        return formatPercentage(this._amount);
      default:
        throw new Error(
          "Invalid memory report(s): bad units in TreeNode.toString"
        );
    }
  },
};

// Sort TreeNodes first by size, then by name.  The latter is important for the
// about:memory tests, which need a predictable ordering of reporters which
// have the same amount.
TreeNode.compareAmounts = function (aA, aB) {
  let a, b;
  if (gIsDiff) {
    a = aA.maxAbsDescendant();
    b = aB.maxAbsDescendant();
  } else {
    a = aA._amount;
    b = aB._amount;
  }
  if (a > b) {
    return -1;
  }
  if (a < b) {
    return 1;
  }
  return TreeNode.compareUnsafeNames(aA, aB);
};

TreeNode.compareUnsafeNames = function (aA, aB) {
  return aA._unsafeName.localeCompare(aB._unsafeName);
};

/**
 * Fill in the remaining properties for the specified tree in a bottom-up
 * fashion.
 *
 * @param aRoot
 *        The tree root.
 */

function fillInTree(aRoot) {
  // Fill in the remaining properties bottom-up.
  function fillInNonLeafNodes(aT) {
    if (!aT._kids) {
      // Leaf node.  Has already been filled in.
    } else if (aT._kids.length === 1 && aT != aRoot) {
      // Non-root, non-leaf node with one child.  Merge the child with the node
      // to avoid redundant entries.
      let kid = aT._kids[0];
      let kidBytes = fillInNonLeafNodes(kid);
      aT._unsafeName += "/" + kid._unsafeName;
      if (kid._kids) {
        aT._kids = kid._kids;
      } else {
        delete aT._kids;
      }
      aT._amount = kidBytes;
      aT._description = kid._description;
      if (kid._nMerged !== undefined) {
        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);
      }

      // ... 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 &&
        (aT._presence === DReport.PRESENT_IN_FIRST_ONLY ||
          aT._presence === DReport.PRESENT_IN_SECOND_ONLY)
      ) {
        aT._amount += kidsBytes;
        let fake = new TreeNode("(fake child)", aT._units);
        fake._presence = DReport.ADDED_FOR_BALANCE;
        fake._amount = aT._amount - kidsBytes;
        aT._kids.push(fake);
        delete aT._presence;
      } else {
        assert(
          aT._amount === undefined,
          "_amount already set for non-leaf node"
        );
        aT._amount = kidsBytes;
      }
      aT._description = "The sum of all entries below this one.";
    }
    return aT._amount;
  }

  // cannotMerge is set because don't want to merge into a tree's root node.
  fillInNonLeafNodes(aRoot);
}

/**
 * Compute the "heap-unclassified" value and insert it into the "explicit"
 * tree.
 *
 * @param aT
 *        The "explicit" tree.
 * @param aHeapAllocatedNode
 *        The "heap-allocated" tree node.
 * @param aHeapTotal
 *        The sum of all explicit HEAP reports for this process.
 * @return A boolean indicating if "heap-allocated" is known for the process.
 */

function addHeapUnclassifiedNode(aT, aHeapAllocatedNode, aHeapTotal) {
  if (aHeapAllocatedNode === undefined) {
    return false;
  }

  if (aT.findKid("heap-unclassified")) {
    // heap-unclassified was already calculated, there's nothing left to do.
    // This can happen when memory reports are exported from areweslimyet.com.
    return true;
  }

  assert(aHeapAllocatedNode._isDegenerate, "heap-allocated is not degenerate");
  let heapAllocatedBytes = aHeapAllocatedNode._amount;
  let heapUnclassifiedT = new TreeNode("heap-unclassified", UNITS_BYTES);
  heapUnclassifiedT._amount = heapAllocatedBytes - aHeapTotal;
  heapUnclassifiedT._description =
    "Memory not classified by a more specific report. This includes " +
    "slop bytes due to internal fragmentation in the heap allocator " +
    "(caused when the allocator rounds up request sizes).";
  aT._kids.push(heapUnclassifiedT);
  aT._amount += heapUnclassifiedT._amount;
  return true;
}

/**
 * Sort all kid nodes from largest to smallest, and insert aggregate nodes
 * where appropriate.
 *
 * @param aTotalBytes
 *        The size of the tree's root node.
 * @param aT
 *        The tree.
 */

function sortTreeAndInsertAggregateNodes(aTotalBytes, aT) {
  const kSignificanceThresholdPerc = 1;

  function isInsignificant(aT) {
    if (gVerbose.checked) {
      return false;
    }

    let perc = gIsDiff
      ? (100 * aT.maxAbsDescendant()) / Math.abs(aTotalBytes)
      : (100 * aT._amount) / aTotalBytes;
    return perc < kSignificanceThresholdPerc;
  }

  if (!aT._kids) {
    return;
  }

  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);
    }
    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);
      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);
      }
      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 = [];

function appendWarningElements(
  aP,
  aHasKnownHeapAllocated,
  aHasMozMallocUsableSize,
  aFiltered
) {
  // These warnings may not make sense if the reporters they reference have been
  // filtered out, so just skip them if we have a filter applied.
  if (!aFiltered && !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 " +
        "shown and the 'explicit' tree shows much less memory than it should.\n\n"
    );
  } else if (!aFiltered && !aHasKnownHeapAllocated) {
    appendElementWithText(
      aP,
      "p",
      "",
      "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\n"
    );
  } else if (!aFiltered && !aHasMozMallocUsableSize) {
    appendElementWithText(
      aP,
      "p",
      "",
      "WARNING: the moz_malloc_usable_size() function does not work for " +
        "this platform and/or configuration. This means that much of the " +
        "heap-allocated memory is not measured by individual memory reporters " +
        "and so will fall under 'heap-unclassified'.\n\n"
    );
  }

  if (gUnsafePathsWithInvalidValuesForThisProcess.length) {
    let div = appendElement(aP, "div");
    appendElementWithText(
      div,
      "p",
      "",
      "WARNING: the following values are negative or unreasonably large.\n"
    );

    let ul = appendElement(div, "ul");
    for (
      let i = 0;
      i < gUnsafePathsWithInvalidValuesForThisProcess.length;
      i++
    ) {
      appendTextNode(ul, " ");
      appendElementWithText(
        ul,
        "li",
        "",
        flipBackslashes(gUnsafePathsWithInvalidValuesForThisProcess[i]) + "\n"
      );
    }

    appendElementWithText(
      div,
      "p",
      "",
      "This indicates a defect in one or more memory reporters. The " +
        "invalid values are highlighted.\n\n"
    );
    gUnsafePathsWithInvalidValuesForThisProcess = []; // reset for the next process
  }
}

/**
 * Appends the about:memory elements for a single process.
 *
 * @param aP
 *        The parent DOM node.
 * @param aN
 *        The number of the process, starting at 0.
 * @param aProcess
 *        The name of the process.
 * @param aTrees
 *        The table of non-degenerate trees for this process.
 * @param aDegenerates
 *        The table of degenerate trees for this process.
 * @param aHasMozMallocUsableSize
 *        Boolean indicating if moz_malloc_usable_size works.
 * @param aFiltered
 *        Boolean indicating whether the reports were filtered.
 * @return The generated text.
 */

function appendProcessAboutMemoryElements(
  aP,
  aN,
  aProcess,
  aTrees,
  aDegenerates,
  aHeapTotal,
  aHasMozMallocUsableSize,
  aFiltered
) {
  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.style = "text-decoration: none";

    // This gives nice spacing when we copy and paste.
    appendElementWithText(aP, "span""""\n");
  };

  appendElementWithText(aP, "h1""", aProcess);
  appendLink("start""end""↓");

  // We'll fill this in later.
  let warningsDiv = appendElement(aP, "div""accuracyWarning");

  // The explicit tree.
  let hasExplicitTree;
  let hasKnownHeapAllocated;
  {
    let treeName = "explicit";
    let t = aTrees[treeName];
    if (t) {
      let pre = appendSectionHeader(aP, "Explicit Allocations");
      hasExplicitTree = true;
      fillInTree(t);
      // Using the "heap-allocated" reporter here instead of
      // nsMemoryReporterManager.heapAllocated goes against the usual pattern.
      // But the "heap-allocated" node will go in the tree like the others, so
      // we have to deal with it, and once we're dealing with it, it's easier
      // to keep doing so rather than switching to the distinguished amount.
      hasKnownHeapAllocated =
        aDegenerates &&
        addHeapUnclassifiedNode(t, aDegenerates["heap-allocated"], aHeapTotal);
      sortTreeAndInsertAggregateNodes(t._amount, t);
      t._description = explicitTreeDescription;
      appendTreeElements(pre, t, aProcess, "");
      delete aTrees[treeName];
    }
    appendTextNode(aP, "\n"); // gives nice spacing when we copy and paste
  }

  // Fill in and sort all the non-degenerate other trees.
  let otherTrees = [];
  for (let unsafeName in aTrees) {
    let t = aTrees[unsafeName];
    assert(!t._isDegenerate, "tree is degenerate");
    fillInTree(t);
    sortTreeAndInsertAggregateNodes(t._amount, t);
    otherTrees.push(t);
  }
  otherTrees.sort(TreeNode.compareUnsafeNames);

  // Get the length of the longest root value among the degenerate other trees,
  // and sort them as well.
  let otherDegenerates = [];
  let maxStringLength = 0;
  for (let unsafeName in aDegenerates) {
    let t = aDegenerates[unsafeName];
    assert(t._isDegenerate, "tree is not degenerate");
    let length = t.toString().length;
    if (length > maxStringLength) {
      maxStringLength = length;
    }
    otherDegenerates.push(t);
  }
  otherDegenerates.sort(TreeNode.compareUnsafeNames);

  // Now generate the elements, putting non-degenerate trees first.
  if (otherTrees.length || otherDegenerates.length) {
    let pre = appendSectionHeader(aP, "Other Measurements");
    for (let t of otherTrees) {
      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, " ");
      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,
      aFiltered
    );
  }

  appendElementWithText(aP, "h3""""End of " + aProcess);
  appendLink("end""start""↑");
}

// The locale used when formatting a number as a human-readable string in any
// format.
const kStyleLocale = "en-US";

// Used for UNITS_BYTES values that are printed as MiB.
const kMBFormat = new Intl.NumberFormat(kStyleLocale, {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
});

// Used for UNITS_PERCENTAGE values.
const kPercFormatter = new Intl.NumberFormat(kStyleLocale, {
  style: "percent",
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
});

// Used for fractions within the tree.
const kFracFormatter = new Intl.NumberFormat(kStyleLocale, {
  style: "percent",
  minimumIntegerDigits: 2,
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
});

// Used for special-casing 100% fractions within the tree.
const kFrac1Formatter = new Intl.NumberFormat(kStyleLocale, {
  style: "percent",
  minimumIntegerDigits: 3,
  minimumFractionDigits: 1,
  maximumFractionDigits: 1,
});

// Used when no custom formatting was requested.
const kDefaultNumFormatter = new Intl.NumberFormat(kStyleLocale);

/**
 * Formats an int as a human-readable string.
 *
 * @param aN
 *        The integer to format.
 * @param aFormatter
 *        Optional formatter object.
 * @return A human-readable string representing the int.
 */

function formatNum(aN, aFormatter) {
  return (aFormatter || kDefaultNumFormatter).format(aN);
}

/**
 * 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), kMBFormat)} MB`;
}

/**
 * Converts a UNITS_PERCENTAGE value 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, kPercFormatter);
}

/*
 * Converts a tree fraction to an appropriate string representation.
 *
 * @param aNum
 *        The numerator.
 * @param aDenom
 *        The denominator.
 * @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, kFrac1Formatter)
    : formatNum(num, kFracFormatter);
}

const kNoKidsSep = " ── ",
  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);
  nameSpan.title = aDescription;

  if (aIsInvalid) {
    let noteText = " [?!]";
    if (!aNMerged) {
      noteText += "\n";
    }
    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}]`;
    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.";
  }

  if (aPresence) {
    let c, title;
    switch (aPresence) {
      case DReport.PRESENT_IN_FIRST_ONLY:
        c = "-";
        title =
          "This value was only present in the first set of memory reports.";
        break;
      case DReport.PRESENT_IN_SECOND_ONLY:
        c = "+";
        title =
          "This value was only present in the second set of memory reports.";
        break;
      case DReport.ADDED_FOR_BALANCE:
        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`);
    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 = {};

function assertClassListContains(aElem, aClassName) {
--> --------------------

--> maximum size reached

--> --------------------

Messung V0.5
C=86 H=88 G=86

¤ Dauer der Verarbeitung: 0.46 Sekunden  (vorverarbeitet)  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge