<?xml version="1.0"?>
<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
<window title="Memory reporters"
        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <script type="application/javascript"
          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>

  <!-- This file tests (in a rough fashion) whether the memory reporters are
       producing sensible results.  test_aboutmemory.xul tests the
       presentation of memory reports in about:memory. -->

  <!-- test results are displayed in the html:body -->
  <body xmlns="http://www.w3.org/1999/xhtml">
  <!-- In bug 773533, <marquee> elements crashed the JS memory reporter -->
  <marquee>Marquee</marquee>
  </body>

  <!-- some URIs that should be anonymized in anonymous mode -->
  <iframe id="amFrame"  height="200" src="http://example.org:80"></iframe>
  <iframe id="amFrame"  height="200" src="https://example.com:443"></iframe>

  <!-- test code goes here -->
  <script type="application/javascript">
  <![CDATA[

  "use strict";

  const Cc = Components.classes;
  const Ci = Components.interfaces;
  const Cr = Components.results;

  const NONHEAP = Ci.nsIMemoryReporter.KIND_NONHEAP;
  const HEAP    = Ci.nsIMemoryReporter.KIND_HEAP;
  const OTHER   = Ci.nsIMemoryReporter.KIND_OTHER;

  const BYTES = Ci.nsIMemoryReporter.UNITS_BYTES;
  const COUNT = Ci.nsIMemoryReporter.UNITS_COUNT;
  const COUNT_CUMULATIVE = Ci.nsIMemoryReporter.UNITS_COUNT_CUMULATIVE;
  const PERCENTAGE = Ci.nsIMemoryReporter.UNITS_PERCENTAGE;

  // Use backslashes instead of forward slashes due to memory reporting's hacky
  // handling of URLs.
  const XUL_NS =
    "http:\\\\www.mozilla.org\\keymaster\\gatekeeper\\there.is.only.xul";

  SimpleTest.waitForExplicitFinish();

  let vsizeAmounts = [];
  let residentAmounts = [];
  let heapAllocatedAmounts = [];
  let storageSqliteAmounts = [];

  let jsGcHeapUsedGcThingsTotal = 0;
  let jsGcHeapUsedGcThings = {};

  let present = {}

  // Generate a long, random string.  We'll check that this string is
  // reported in at least one of the memory reporters.
  let bigString = "";
  while (bigString.length < 10000) {
    bigString += Math.random();
  }
  let bigStringPrefix = bigString.substring(0, 100);

  // Generate many copies of two distinctive short strings, "!)(*&" and
  // "@)(*&".  We'll check that these strings are reported in at least
  // one of the memory reporters.
  let shortStrings = [];
  for (let i = 0; i < 10000; i++) {
    let str = (Math.random() > 0.5 ? "!" : "@") + ")(*&";
    shortStrings.push(str);
  }

  let mySandbox = Components.utils.Sandbox(document.nodePrincipal,
                    { sandboxName: "this-is-a-sandbox-name" });

  function handleReportNormal(aProcess, aPath, aKind, aUnits, aAmount,
                              aDescription)
  {
    // Record the values of some notable reporters.
    if (aPath === "vsize") {
      vsizeAmounts.push(aAmount);
    } else if (aPath === "resident") {
      residentAmounts.push(aAmount);
    } else if (aPath.search(/^js-main-runtime-gc-heap-committed\/used\/gc-things\//) >= 0) {
      jsGcHeapUsedGcThingsTotal += aAmount;
      jsGcHeapUsedGcThings[aPath] = (jsGcHeapUsedGcThings[aPath] | 0) + 1;
    } else if (aPath === "heap-allocated") {
      heapAllocatedAmounts.push(aAmount);
    } else if (aPath === "storage-sqlite") {
      storageSqliteAmounts.push(aAmount);

    // Check the presence of some other notable reporters.
    } else if (aPath.search(/^explicit\/js-non-window\/.*compartment\(/) >= 0) {
      present.jsNonWindowCompartments = true;
    } else if (aPath.search(/^explicit\/window-objects\/top\(.*\/js-compartment\(/) >= 0) {
      present.windowObjectsJsCompartments = true;
    } else if (aPath.search(/^explicit\/storage\/sqlite\/places.sqlite/) >= 0) {
      present.places = true;
    } else if (aPath.search(/^explicit\/images/) >= 0) {
      present.images = true;
    } else if (aPath.search(/^explicit\/xpti-working-set$/) >= 0) {
      present.xptiWorkingSet = true;
    } else if (aPath.search(/^explicit\/atom-tables\/main$/) >= 0) {
      present.atomTablesMain = true;
    } else if (/\[System Principal\].*this-is-a-sandbox-name/.test(aPath)) {
      // A system compartment with a location (such as a sandbox) should
      // show that location.
      present.sandboxLocation = true;
    } else if (aPath.includes(bigStringPrefix)) {
      present.bigString = true;
    } else if (aPath.includes("!)(*&")) {
      present.smallString1 = true;
    } else if (aPath.includes("@)(*&")) {
      present.smallString2 = true;
    }

    // Shouldn't get any anonymized paths.
    if (aPath.includes('<anonymized')) {
        present.anonymizedWhenUnnecessary = aPath;
    }
  }

  function handleReportAnonymized(aProcess, aPath, aKind, aUnits, aAmount,
                                  aDescription)
  {
    // Path might include an xmlns using http, which is safe to ignore.
    let reducedPath = aPath.replace(XUL_NS, "");

    // Shouldn't get http: or https: in any paths.
    if (reducedPath.includes('http:')) {
        present.httpWhenAnonymized = aPath;
    }

    // file: URLs should have their path anonymized.
    if (reducedPath.search('file:..[^<]') !== -1) {
        present.unanonymizedFilePathWhenAnonymized = aPath;
    }
  }

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

  let amounts = [
    "vsize",
    "vsizeMaxContiguous",
    "resident",
    "residentFast",
    "residentPeak",
    "residentUnique",
    "heapAllocated",
    "heapOverheadFraction",
    "JSMainRuntimeGCHeap",
    "JSMainRuntimeTemporaryPeak",
    "JSMainRuntimeCompartmentsSystem",
    "JSMainRuntimeCompartmentsUser",
    "imagesContentUsedUncompressed",
    "storageSQLite",
    "lowMemoryEventsVirtual",
    "lowMemoryEventsPhysical",
    "ghostWindows",
    "pageFaultsHard",
  ];
  for (let i = 0; i < amounts.length; i++) {
    try {
      // If mgr[amounts[i]] throws an exception, just move on -- some amounts
      // aren't available on all platforms.  But if the attribute simply
      // isn't present, that indicates the distinguished amounts have changed
      // and this file hasn't been updated appropriately.
      let dummy = mgr[amounts[i]];
      ok(dummy !== undefined,
         "accessed an unknown distinguished amount: " + amounts[i]);
    } catch (ex) {
    }
  }

  // Run sizeOfTab() to make sure it doesn't crash.  We can't check the result
  // values because they're non-deterministic.
  let jsObjectsSize = {};
  let jsStringsSize = {};
  let jsOtherSize = {};
  let domSize = {};
  let styleSize = {};
  let otherSize = {};
  let totalSize = {};
  let jsMilliseconds = {};
  let nonJSMilliseconds = {};
  mgr.sizeOfTab(window, jsObjectsSize, jsStringsSize, jsOtherSize,
                domSize, styleSize, otherSize, totalSize,
                jsMilliseconds, nonJSMilliseconds);

  let asyncSteps = [
    getReportsNormal,
    getReportsAnonymized,
    checkResults,
    test_register_strong,
    test_register_strong, // Make sure re-registering works
    test_register_weak,
    SimpleTest.finish
  ];

  function runNext() {
    setTimeout(asyncSteps.shift(), 0);
  }

  function getReportsNormal()
  {
    mgr.getReports(handleReportNormal, null,
                   runNext, null,
                   /* anonymize = */ false);
  }

  function getReportsAnonymized()
  {
    mgr.getReports(handleReportAnonymized, null,
                   runNext, null,
                   /* anonymize = */ true);
  }

  function checkSizeReasonable(aName, aAmount)
  {
    // Check the size is reasonable -- i.e. not ridiculously large or small.
    ok(100 * 1000 <= aAmount && aAmount <= 10 * 1000 * 1000 * 1000,
       aName + "'s size is reasonable");
  }

  function checkSpecialReport(aName, aAmounts, aCanBeUnreasonable)
  {
    ok(aAmounts.length == 1, aName + " has " + aAmounts.length + " report");
    let n = aAmounts[0];
    if (!aCanBeUnreasonable) {
      checkSizeReasonable(aName, n);
    }
  }

  function checkResults()
  {
    try {
      // Nb: mgr.heapAllocated will throw NS_ERROR_NOT_AVAILABLE if this is a
      // --disable-jemalloc build.  Allow for skipping this test on that
      // exception, but *only* that exception.
      let dummy = mgr.heapAllocated;
      checkSpecialReport("heap-allocated", heapAllocatedAmounts);
    } catch (ex) {
      is(ex.result, Cr.NS_ERROR_NOT_AVAILABLE, "mgr.heapAllocated exception");
    }
    // vsize may be unreasonable if ASAN is enabled
    checkSpecialReport("vsize",          vsizeAmounts, /*canBeUnreasonable*/true);
    checkSpecialReport("resident",       residentAmounts);

    for (var reporter in jsGcHeapUsedGcThings) {
      ok(jsGcHeapUsedGcThings[reporter] == 1);
    }
    checkSizeReasonable("js-main-runtime-gc-heap-committed/used/gc-things",
                        jsGcHeapUsedGcThingsTotal);

    ok(present.jsNonWindowCompartments,     "js-non-window compartments are present");
    ok(present.windowObjectsJsCompartments, "window-objects/.../js compartments are present");
    ok(present.places,                      "places is present");
    ok(present.images,                      "images is present");
    ok(present.xptiWorkingSet,              "xpti-working-set is present");
    ok(present.atomTablesMain,              "atom-tables/main is present");
    ok(present.sandboxLocation,             "sandbox locations are present");
    ok(present.bigString,                   "large string is present");
    ok(present.smallString1,                "small string 1 is present");
    ok(present.smallString2,                "small string 2 is present");

    ok(!present.anonymizedWhenUnnecessary,
       "anonymized paths are not present when unnecessary. Failed case: " +
       present.anonymizedWhenUnnecessary);
    ok(!present.httpWhenAnonymized,
       "http URLs are anonymized when necessary. Failed case: " +
       present.httpWhenAnonymized);
    ok(!present.unanonymizedFilePathWhenAnonymized,
       "file URLs are anonymized when necessary. Failed case: " +
       present.unanonymizedFilePathWhenAnonymized);

    runNext();
  }

  // Reporter registration tests

  // collectReports() calls to the test reporter.
  let called = 0;

  // The test memory reporter, testing the various report units.
  // Also acts as a report collector, verifying the reported values match the
  // expected ones after passing through XPConnect / nsMemoryReporterManager
  // and back.
  function MemoryReporterAndCallback() {
    this.seen = 0;
  }
  MemoryReporterAndCallback.prototype = {
    // The test reports.
    // Each test key corresponds to the path of the report.  |amount| is a
    // function called when generating the report.  |expected| is a function
    // to be tested when receiving a report during collection.  If |expected| is
    // omitted the |amount| will be checked instead.
    tests: {
      "test-memory-reporter-bytes1": {
        units: BYTES,
        amount: () => 0
      },
      "test-memory-reporter-bytes2": {
        units: BYTES,
        amount: () => (1<<30) * 8 // awkward way to say 8G in JS
      },
      "test-memory-reporter-counter": {
        units: COUNT,
        amount: () => 2
      },
      "test-memory-reporter-ccounter": {
        units: COUNT_CUMULATIVE,
        amount: () => ++called,
        expected: () => called
      },
      "test-memory-reporter-percentage": {
        units: PERCENTAGE,
        amount: () => 9999
      }
    },
    // nsIMemoryReporter
    collectReports: function(callback, data, anonymize) {
      for (let path of Object.keys(this.tests)) {
        try {
          let test = this.tests[path];
          callback.callback(
            "", // Process. Should be "" initially.
            path,
            OTHER,
            test.units,
            test.amount(),
            "Test " + path + ".",
            data);
        }
        catch (ex) {
          ok(false, ex);
        }
      }
    },
    // nsIMemoryReporterCallback
    callback: function(process, path, kind, units, amount, data) {
      if (path in this.tests) {
        this.seen++;
        let test = this.tests[path];
        ok(units === test.units, "Test reporter units match");
        ok(amount === (test.expected || test.amount)(),
           "Test reporter values match: " + amount);
      }
    },
    // Checks that the callback has seen the expected number of reports, and
    // resets the callback counter.
    // @param expected  Optional.  Expected number of reports the callback
    //                  should have processed.
    finish: function(expected) {
      if (expected === undefined) {
        expected = Object.keys(this.tests).length;
      }
      is(expected, this.seen,
         "Test reporter called the correct number of times: " + expected);
      this.seen = 0;
    }
  };

  // General memory reporter + registerStrongReporter tests.
  function test_register_strong() {
    let reporterAndCallback = new MemoryReporterAndCallback();
    // Registration works.
    mgr.registerStrongReporter(reporterAndCallback);

    // Check the generated reports.
    mgr.getReports(reporterAndCallback, null,
      () => {
        reporterAndCallback.finish();
        window.setTimeout(test_unregister_strong, 0, reporterAndCallback);
      }, null,
      /* anonymize = */ false);
  }

  function test_unregister_strong(aReporterAndCallback)
  {
    mgr.unregisterStrongReporter(aReporterAndCallback);

    // The reporter was unregistered, hence there shouldn't be any reports from
    // the test reporter.
    mgr.getReports(aReporterAndCallback, null,
      () => {
        aReporterAndCallback.finish(0);
        runNext();
      }, null,
      /* anonymize = */ false);
  }

  // Check that you cannot register JS components as weak reporters.
  function test_register_weak() {
    let reporterAndCallback = new MemoryReporterAndCallback();
    try {
      // Should fail! nsMemoryReporterManager will only hold a raw pointer to
      // "weak" reporters.  When registering a weak reporter, XPConnect will
      // create a WrappedJS for JS components.  This WrappedJS would be
      // successfully registered with the manager, only to be destroyed
      // immediately after, which would eventually lead to a crash when
      // collecting the reports.  Therefore nsMemoryReporterManager should
      // reject WrappedJS reporters, which is what is tested here.
      // See bug 950391 comment #0.
      mgr.registerWeakReporter(reporterAndCallback);
      ok(false, "Shouldn't be allowed to register a JS component (WrappedJS)");
    }
    catch (ex) {
      ok(ex.message.indexOf("NS_ERROR_") >= 0,
         "WrappedJS reporter got rejected: " + ex);
    }

    runNext();
  }

  // Kick-off the async tests.
  runNext();

  ]]>
  </script>
</window>