/* import-globals-from ../shared-head.js */

Services.scriptloader.loadSubScript(
  "chrome://mochitests/content/browser/tools/profiler/tests/browser/shared-head.js",
  this
);

const BASE_URL = "http://example.com/browser/tools/profiler/tests/browser/";
const BASE_URL_HTTPS =
  "https://example.com/browser/tools/profiler/tests/browser/";

registerCleanupFunction(async () => {
  if (Services.profiler.IsActive()) {
    info(
      "The profiler was found to still be running at the end of the test, which means that some error likely occured. Let's stop it to prevent issues with following tests!"
    );
    await Services.profiler.StopProfiler();
  }
});

/**
 * This is a helper function that will stop the profiler and returns the main
 * threads for the parent process and the content process with PID contentPid.
 * This happens immediately, without waiting for any sampling to happen or
 * finish. Use waitSamplingAndStopProfilerAndGetThreads below instead to wait
 * for samples before stopping.
 * This returns also the full profile in case the caller wants more information.
 *
 * @param {number} contentPid
 * @returns {Promise<{profile, parentThread, contentProcess, contentThread}>}
 */
async function stopProfilerNowAndGetThreads(contentPid) {
  const profile = await stopNowAndGetProfile();

  const parentThread = profile.threads[0];
  const contentProcess = profile.processes.find(
    p => p.threads[0].pid == contentPid
  );
  if (!contentProcess) {
    throw new Error(
      `Could not find the content process with given pid: ${contentPid}`
    );
  }

  if (!parentThread) {
    throw new Error("The parent thread was not found in the profile.");
  }

  const contentThread = contentProcess.threads[0];
  if (!contentThread) {
    throw new Error("The content thread was not found in the profile.");
  }

  return { profile, parentThread, contentProcess, contentThread };
}

/**
 * This is a helper function that will stop the profiler and returns the main
 * threads for the parent process and the content process with PID contentPid.
 * As opposed to stopProfilerNowAndGetThreads (with "Now") above, the profiler
 * in that PID will not stop until there is at least one periodic sample taken.
 *
 * @param {number} contentPid
 * @returns {Promise<{profile, parentThread, contentProcess, contentThread}>}
 */
async function waitSamplingAndStopProfilerAndGetThreads(contentPid) {
  await Services.profiler.waitOnePeriodicSampling();

  return stopProfilerNowAndGetThreads(contentPid);
}

/** This tries to find the service worker thread by targeting a very specific
 * UserTiming marker. Indeed we use performance.mark to add this marker from the
 * service worker's events.
 * Then from this thread we get its parent thread. Indeed the parent thread is
 * where all network stuff happens, so this is useful for network marker tests.
 *
 * @param {Object} profile
 * @returns {{ serviceWorkerThread: Object, serviceWorkerParentThread: Object }} the found threads
 */
function findServiceWorkerThreads(profile) {
  const allThreads = [
    profile.threads,
    ...profile.processes.map(process => process.threads),
  ].flat();

  const serviceWorkerThread = allThreads.find(
    ({ processType, markers }) =>
      processType === "tab" &&
      markers.data.some(markerTuple => {
        const data = markerTuple[markers.schema.data];
        return (
          data &&
          data.type === "UserTiming" &&
          data.name === "__serviceworker_event"
        );
      })
  );

  if (!serviceWorkerThread) {
    info(
      "We couldn't find a service worker thread. Here are all the threads in this profile:"
    );
    allThreads.forEach(logInformationForThread.bind(null, ""));
    return null;
  }

  const serviceWorkerParentThread = allThreads.find(
    ({ name, pid }) => pid === serviceWorkerThread.pid && name === "GeckoMain"
  );

  if (!serviceWorkerParentThread) {
    info(
      `We couldn't find a parent thread for the service worker thread (pid: ${serviceWorkerThread.pid}, tid: ${serviceWorkerThread.tid}).`
    );
    info("Here are all the threads in this profile:");
    allThreads.forEach(logInformationForThread.bind(null, ""));

    // Let's write the profile on disk if MOZ_UPLOAD_DIR is present
    const path = Services.env.get("MOZ_UPLOAD_DIR");
    if (path) {
      const profileName = `profile_${Date.now()}.json`;
      const profilePath = PathUtils.join(path, profileName);
      info(
        `We wrote down the profile on disk as an artifact, with name ${profileName}.`
      );
      // This function returns a Promise, but we're not waiting on it because
      // we're in a synchronous function. Hopefully writing will be finished
      // when the process ends.
      IOUtils.writeJSON(profilePath, profile).catch(err =>
        console.error("An error happened when writing the profile on disk", err)
      );
    }
    throw new Error(
      "We couldn't find a parent thread for the service worker thread. Please read logs to find more information."
    );
  }

  return { serviceWorkerThread, serviceWorkerParentThread };
}

/**
 * This logs some basic information about the passed thread.
 *
 * @param {string} prefix
 * @param {Object} thread
 */
function logInformationForThread(prefix, thread) {
  if (!thread) {
    info(prefix + ": thread is null or undefined.");
    return;
  }

  const { name, pid, tid, processName, processType } = thread;
  info(
    `${prefix}: ` +
      `name(${name}) pid(${pid}) tid(${tid}) processName(${processName}) processType(${processType})`
  );
}
