/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

// Load the tracker from a distinct compartment so that it can inspect any other
// module/compartment, even DevTools, chrome, and this script!
const {
  useDistinctSystemPrincipalLoader,
  releaseDistinctSystemPrincipalLoader,
} = ChromeUtils.importESModule(
  "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
  { global: "shared" }
);

const requester = {};
const loader = useDistinctSystemPrincipalLoader(requester);
registerCleanupFunction(() => releaseDistinctSystemPrincipalLoader(requester));

const { allocationTracker } = loader.require(
  "chrome://mochitests/content/browser/devtools/shared/test-helpers/allocation-tracker"
);
const TrackedObjects = loader.require(
  "resource://devtools/shared/test-helpers/tracked-objects.sys.mjs"
);

// This test record multiple times complete heap snapshot,
// so that it can take a little bit to complete.
requestLongerTimeout(2);

add_task(async function () {
  // Use a sandbox to allocate test javascript object in order to avoid any
  // external noise
  const global = Cu.Sandbox("http://example.com");

  const tracker = allocationTracker({ watchGlobal: global });
  const before = tracker.stillAllocatedObjects();

  /* eslint-disable no-undef */
  // This will allocation 1001 objects. The array and 1000 elements in it.
  Cu.evalInSandbox(
    "let list; new " +
      function () {
        list = [];
        for (let i = 0; i < 1000; i++) {
          list.push({});
        }
      },
    global,
    undefined,
    "test-file.js",
    1,
    /* enforceFilenameRestrictions */ false
  );
  /* eslint-enable no-undef */

  const allocations = tracker.countAllocations();
  Assert.greaterOrEqual(
    allocations,
    1001,
    `At least 1001 objects are reported as created (${allocations})`
  );

  // Uncomment this and comment the call to `countAllocations` to debug the allocations.
  // The call to `countAllocations` will reset the allocation record.
  // tracker.logAllocationSites();

  const afterCreation = tracker.stillAllocatedObjects();
  is(
    afterCreation.objectsWithStack - before.objectsWithStack,
    1001,
    "We got exactly the expected number of objects recorded with an allocation site"
  );
  Assert.greater(
    afterCreation.objectsWithStack,
    before.objectsWithStack,
    "We got some random number of objects without an allocation site"
  );

  Cu.evalInSandbox(
    "list = null;",
    global,
    undefined,
    "test-file.js",
    7,
    /* enforceFilenameRestrictions */ false
  );

  Cu.forceGC();
  Cu.forceCC();

  const afterGC = tracker.stillAllocatedObjects();
  is(
    afterCreation.objectsWithStack - afterGC.objectsWithStack,
    1001,
    "All the expected objects were reported freed in the count with allocation sites"
  );
  Assert.less(
    afterGC.objectsWithoutStack,
    afterCreation.objectsWithoutStack,
    "And we released some random number of objects without an allocation site"
  );

  tracker.stop();
});

add_task(async function () {
  const leaked = {};
  TrackedObjects.track(leaked);
  let transient = {};
  TrackedObjects.track(transient);

  is(
    TrackedObjects.getStillAllocatedObjects().length,
    2,
    "The two objects are reported"
  );

  info("Free the transient object");
  transient = null;
  Cu.forceGC();

  is(
    TrackedObjects.getStillAllocatedObjects().length,
    1,
    "We now only have the leaked object"
  );
  is(
    TrackedObjects.getStillAllocatedObjects()[0].weakRef.get(),
    leaked,
    "The still allocated objects is the leaked one"
  );
  TrackedObjects.clear();
});

add_task(async function () {
  info("Test start and stop recording without any debug mode");
  const tracker = allocationTracker({ watchDevToolsGlobals: true });
  await tracker.startRecordingAllocations();
  await tracker.stopRecordingAllocations();
  tracker.stop();
});

add_task(async function () {
  info("Test start and stop recording with 'allocations' debug mode");
  const tracker = allocationTracker({ watchDevToolsGlobals: true });
  await tracker.startRecordingAllocations("allocations");
  await tracker.stopRecordingAllocations("allocations");
  tracker.stop();
});

add_task(async function () {
  info("Test start and stop recording with 'leaks' debug mode");
  const tracker = allocationTracker({ watchDevToolsGlobals: true });
  await tracker.startRecordingAllocations("leaks");
  await tracker.stopRecordingAllocations("leaks");
  tracker.stop();
});

add_task(async function () {
  info("Test start and stop recording with tracked objects");

  const leaked = {};
  TrackedObjects.track(leaked);

  const tracker = allocationTracker({ watchAllGlobals: true });
  await tracker.startRecordingAllocations();
  await tracker.stopRecordingAllocations();
  tracker.stop();

  TrackedObjects.clear();
});

add_task(async function () {
  info("Test start and stop recording with tracked objects");

  const sandbox = Cu.Sandbox(window);
  const tracker = allocationTracker({ watchGlobal: sandbox });
  await tracker.startRecordingAllocations("leaks");

  Cu.evalInSandbox("this.foo = {};", sandbox, null, "sandbox.js", 1);

  const record = await tracker.stopRecordingAllocations("leaks");
  is(
    record.objectsWithStack,
    1,
    "We get only one leaked objects, the foo object of the sandbox."
  );
  Assert.greater(
    record.objectsWithoutStack,
    10,
    "We get an handful of objects without stacks. Most likely created by Memory API itself."
  );

  is(
    record.leaks.length,
    2,
    "We get the one leak and the objects with missing stacks"
  );
  is(
    record.leaks[0].src,
    "UNKNOWN",
    "First item is the objects with missing stacks"
  );
  // In theory the two following values should be equal,
  // but they aren't always because of some dark matter around objects with missing stacks.
  // `count` is computed out of `takeCensus`, while `objectsWithoutStack` uses `drainAllocationsLog`
  // While the first go through the current GC graph, the second is a record of allocations over time,
  // this probably explain why there is some subtle difference
  Assert.lessOrEqual(
    record.leaks[0].count,
    record.objectsWithoutStack,
    "For now, the leak report intermittently assume there is less leaked objects than the summary"
  );
  is(record.leaks[1].src, "sandbox.js", "Second item if about our 'foo' leak");
  is(record.leaks[1].count, 1, "We leak one object on this file");
  is(record.leaks[1].lines.length, 1, "We leak from only one line");
  is(record.leaks[1].lines[0], "1: 1", "On first line, we leak one object");
  tracker.stop();

  TrackedObjects.clear();
});

add_task(async function () {
  info("Test that transient globals are not leaked");

  const tracker = allocationTracker({ watchAllGlobals: true });

  let sandboxBefore = Cu.Sandbox(window);
  // We need to allocate at least one object from the global to reproduce the leak
  Cu.evalInSandbox(
    "this.foo = {};",
    sandboxBefore,
    null,
    "sandbox-before.js",
    1
  );
  const weakBefore = Cu.getWeakReference(sandboxBefore);
  sandboxBefore = null;

  await tracker.startRecordingAllocations();

  ok(
    !weakBefore.get(),
    "Sandbox created before the record should have been freed by GCs done by startRecordingAllocations"
  );

  let sandboxDuring = Cu.Sandbox(window);
  // We need to allocate at least one object from the global to reproduce the leak
  Cu.evalInSandbox(
    "this.bar = {};",
    sandboxDuring,
    null,
    "sandbox-during.js",
    1
  );
  const weakDuring = Cu.getWeakReference(sandboxDuring);
  sandboxDuring = null;

  await tracker.stopRecordingAllocations();

  ok(
    !weakDuring.get(),
    "Sandbox should have been freed by GCs done by stopRecordingAllocations"
  );

  tracker.stop();
});
