/* 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/. */

"use strict";

// Recording already set preferences.
const devtoolsPreferences = Services.prefs.getBranch("devtools");
const alreadySetPreferences = new Set();
for (const pref of devtoolsPreferences.getChildList("")) {
  if (devtoolsPreferences.prefHasUserValue(pref)) {
    alreadySetPreferences.add(pref);
  }
}

// Reset all devtools preferences on test end.
registerCleanupFunction(async () => {
  await SpecialPowers.flushPrefEnv();

  // Reset devtools preferences modified by the test.
  for (const pref of devtoolsPreferences.getChildList("")) {
    if (
      devtoolsPreferences.prefHasUserValue(pref) &&
      !alreadySetPreferences.has(pref)
    ) {
      devtoolsPreferences.clearUserPref(pref);
    }
  }
});

// Ignore promise rejections for actions triggered after panels are closed.
{
  const { PromiseTestUtils } = ChromeUtils.importESModule(
    "resource://testing-common/PromiseTestUtils.sys.mjs"
  );
  PromiseTestUtils.allowMatchingRejectionsGlobally(
    /REDUX_MIDDLEWARE_IGNORED_REDUX_ACTION/
  );
}

// Load the tracker very first in order to ensure tracking all objects created by DevTools.
// This is especially important for allocation sites. We need to catch the global the
// earliest possible in order to ensure that all allocation objects come with a stack.
//
// If we want to track DevTools module loader we should ensure loading Loader.sys.mjs within
// the `testScript` Function. i.e. after having calling startRecordingAllocations.
let tracker, releaseTrackerLoader;
{
  const {
    useDistinctSystemPrincipalLoader,
    releaseDistinctSystemPrincipalLoader,
  } = ChromeUtils.importESModule(
    "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
    { global: "shared" }
  );

  const requester = {};
  const loader = useDistinctSystemPrincipalLoader(requester);
  releaseTrackerLoader = () => releaseDistinctSystemPrincipalLoader(requester);
  const { allocationTracker } = loader.require(
    "chrome://mochitests/content/browser/devtools/shared/test-helpers/allocation-tracker.js"
  );
  tracker = allocationTracker({ watchDevToolsGlobals: true });
}

// /!\ Be careful about imports/require
//
// Some tests may record the very first time we load a module.
// If we start loading them from here, we might only retrieve the already loaded
// module from the loader's cache. This would no longer highlight the cost
// of loading a new module from scratch.
//
// => Avoid loading devtools module as much as possible
// => If you really have to, lazy load them

ChromeUtils.defineLazyGetter(this, "TrackedObjects", () => {
  return ChromeUtils.importESModule(
    "resource://devtools/shared/test-helpers/tracked-objects.sys.mjs"
  );
});

ChromeUtils.defineLazyGetter(this, "TraceObjects", () => {
  return ChromeUtils.importESModule(
    "chrome://mochitests/content/browser/devtools/shared/test-helpers/trace-objects.sys.mjs"
  );
});

// So that PERFHERDER data can be extracted from the logs.
SimpleTest.requestCompleteLog();

// We have to disable testing mode, or various debug instructions are enabled.
// We especially want to disable redux store history, which would leak all the actions!
SpecialPowers.pushPrefEnv({
  set: [["devtools.testing", false]],
});

// Set DEBUG_DEVTOOLS_ALLOCATIONS=allocations|leaks in order print debug informations.
const DEBUG_ALLOCATIONS = Services.env.get("DEBUG_DEVTOOLS_ALLOCATIONS");

async function addTab(url) {
  const tab = BrowserTestUtils.addTab(gBrowser, url);
  gBrowser.selectedTab = tab;
  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
  return tab;
}

/**
 * This function will force some garbage collection before recording
 * data about allocated objects.
 *
 * This accept an optional boolean to also record the content process objects
 * of the current tab. That, in addition of objects from the parent process,
 * which are always recorded.
 *
 * This return same data object which is meant to be passed to `stopRecordingAllocations` as-is.
 *
 * See README.md file in this folder.
 */
async function startRecordingAllocations({
  alsoRecordContentProcess = false,
} = {}) {
  // Also start recording allocations in the content process, if requested
  if (alsoRecordContentProcess) {
    await SpecialPowers.spawn(
      gBrowser.selectedBrowser,
      [DEBUG_ALLOCATIONS],
      async debug_allocations => {
        const { DevToolsLoader } = ChromeUtils.importESModule(
          "resource://devtools/shared/loader/Loader.sys.mjs"
        );

        const {
          useDistinctSystemPrincipalLoader,
          releaseDistinctSystemPrincipalLoader,
        } = ChromeUtils.importESModule(
          "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
        );

        const requester = {};
        const loader = useDistinctSystemPrincipalLoader(requester);
        const { allocationTracker } = loader.require(
          "chrome://mochitests/content/browser/devtools/shared/test-helpers/allocation-tracker.js"
        );
        // We watch all globals in the content process, (instead of only DevTools global in parent process)
        // because we may easily leak web page objects, which aren't in DevTools global.
        const tracker = allocationTracker({ watchAllGlobals: true });

        // /!\ HACK: store tracker and releaseTrackerLoader on DevToolsLoader in order
        // to be able to reuse them in a following call to SpecialPowers.spawn
        DevToolsLoader.tracker = tracker;
        DevToolsLoader.releaseTrackerLoader = () =>
          releaseDistinctSystemPrincipalLoader(requester);

        await tracker.startRecordingAllocations(debug_allocations);
      }
    );
    // Trigger a GC in the parent process as this additional ContentTask
    // seems to make harder to release objects created before we start recording.
    await tracker.doGC();
  }

  await tracker.startRecordingAllocations(DEBUG_ALLOCATIONS);
}

/**
 * See doc of startRecordingAllocations
 */
async function stopRecordingAllocations(
  recordName,
  { alsoRecordContentProcess = false } = {}
) {
  // Ensure that Memory API didn't ran out of buffers
  ok(!tracker.overflowed, "Allocation were all recorded in the parent process");

  // And finally, retrieve the record *after* having ran the test
  const parentProcessData =
    await tracker.stopRecordingAllocations(DEBUG_ALLOCATIONS);

  const leakedObjects = TrackedObjects.getStillAllocatedObjects();
  if (leakedObjects.length) {
    await TraceObjects.traceObjects(leakedObjects, tracker.getSnapshotFile());
  }

  let contentProcessData = null;
  if (alsoRecordContentProcess) {
    contentProcessData = await SpecialPowers.spawn(
      gBrowser.selectedBrowser,
      [DEBUG_ALLOCATIONS],
      debug_allocations => {
        const { DevToolsLoader } = ChromeUtils.importESModule(
          "resource://devtools/shared/loader/Loader.sys.mjs"
        );
        const { tracker } = DevToolsLoader;
        ok(
          !tracker.overflowed,
          "Allocation were all recorded in the content process"
        );
        return tracker.stopRecordingAllocations(debug_allocations);
      }
    );
  }

  const trackedObjectsInContent = await SpecialPowers.spawn(
    gBrowser.selectedBrowser,
    [],
    async () => {
      const TrackedObjects = ChromeUtils.importESModule(
        "resource://devtools/shared/test-helpers/tracked-objects.sys.mjs"
      );
      const leakedObjects = TrackedObjects.getStillAllocatedObjects();
      if (leakedObjects.length) {
        const TraceObjects = ChromeUtils.importESModule(
          "chrome://mochitests/content/browser/devtools/shared/test-helpers/trace-objects.sys.mjs"
        );
        // Only pass 'weakRef' as Memory API and 'ubiNodeId' can only be inspected in the parent process
        await TraceObjects.traceObjects(
          leakedObjects.map(e => {
            return {
              weakRef: e.weakRef,
            };
          })
        );
        const { DevToolsLoader } = ChromeUtils.importESModule(
          "resource://devtools/shared/loader/Loader.sys.mjs"
        );
        const { tracker } = DevToolsLoader;
        // Record the heap snapshot from the content process,
        // and pass the record's filepath to the parent process
        // As only the parent process can read the file because
        // of sandbox restrictions made to content processes regarding file I/O.
        const snapshotFile = tracker.getSnapshotFile();
        return {
          snapshotFile,
          // Only pass ubi::Node::Id from this content process to the parent process.
          // `leakedObjects`'s `weakRef` attributes can't be transferred across processes.
          // TraceObjects.traceObjects in the parent process will only log leaks
          // via the Memory API (and Node Id's).
          objectUbiNodeIds: leakedObjects.map(e => {
            return {
              ubiNodeId: e.ubiNodeId,
            };
          }),
        };
      }
      return null;
    }
  );
  if (trackedObjectsInContent) {
    TraceObjects.traceObjects(
      trackedObjectsInContent.objectUbiNodeIds,
      trackedObjectsInContent.snapshotFile
    );
  }

  // Craft the JSON object required to save data in talos database
  info(
    `The ${recordName} test leaked ${parentProcessData.objectsWithStack} objects (${parentProcessData.objectsWithoutStack} with missing allocation site) in the parent process`
  );
  const PERFHERDER_DATA = {
    framework: {
      name: "devtools",
    },
    suites: [
      {
        name: recordName + ":parent-process",
        subtests: [
          {
            name: "objects-with-stacks",
            value: parentProcessData.objectsWithStack,
          },
          {
            name: "memory",
            value: parentProcessData.memory,
          },
        ],
      },
    ],
  };
  if (alsoRecordContentProcess) {
    info(
      `The ${recordName} test leaked ${contentProcessData.objectsWithStack} objects (${contentProcessData.objectsWithoutStack} with missing allocation site) in the content process`
    );
    PERFHERDER_DATA.suites.push({
      name: recordName + ":content-process",
      subtests: [
        {
          name: "objects-with-stacks",
          value: contentProcessData.objectsWithStack,
        },
        {
          name: "memory",
          value: contentProcessData.memory,
        },
      ],
    });

    // Finally release the tracker loader in content process.
    await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
      const { DevToolsLoader } = ChromeUtils.importESModule(
        "resource://devtools/shared/loader/Loader.sys.mjs"
      );
      DevToolsLoader.releaseTrackerLoader();
    });
  }

  // And release the tracker loader in the parent process
  releaseTrackerLoader();

  // Log it to stdout so that perfherder can collect this data.
  // This only works if we called `SimpleTest.requestCompleteLog()`!
  info("PERFHERDER_DATA: " + JSON.stringify(PERFHERDER_DATA));
}
