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

"use strict";

/**
 * Return a web-based URL for a given file based on the testing directory.
 * @param {String} fileName
 *        file that caller wants its web-based url
 */
function GetTestWebBasedURL(fileName) {
  return (
    getRootDirectory(gTestPath).replace(
      "chrome://mochitests/content",
      "https://example.org"
    ) + fileName
  );
}

const kNS_PER_MS = 1000000;

function printProcInfo(procInfo) {
  info(
    `  pid: ${procInfo.pid}, type = parent, cpu time = ${
      procInfo.cpuTime / kNS_PER_MS
    }ms`
  );
  for (let child of procInfo.children) {
    info(
      `  pid: ${child.pid}, type = ${child.type}, cpu time = ${
        child.cpuTime / kNS_PER_MS
      }ms`
    );
  }
}

// It would be nice to have an API to list all the statically defined labels of
// a labeled_counter. Hopefully bug 1672273 will help with this.
const kGleanProcessTypeLabels = [
  "parent.active",
  "parent.active.playing-audio",
  "parent.active.playing-video",
  "parent.inactive",
  "parent.inactive.playing-audio",
  "parent.inactive.playing-video",
  "prealloc",
  "privilegedabout",
  "rdd",
  "socket",
  "web.background",
  "web.background-perceivable",
  "web.foreground",
  "extension",
  "gpu",
  "gmplugin",
  "utility",
  "__other__",
];

async function getChildCpuTime(pid) {
  return (await ChromeUtils.requestProcInfo()).children.find(p => p.pid == pid)
    .cpuTime;
}

add_task(async () => {
  // Temporarily open a new foreground tab to make the current tab a background
  // tab, and burn some CPU time in it while it's in the background.
  const kBusyWaitForMs = 50;
  let cpuTimeSpentOnBackgroundTab;
  let firstBrowser = gBrowser.selectedTab.linkedBrowser;
  let processPriorityChangedPromise = BrowserTestUtils.contentTopicObserved(
    firstBrowser.browsingContext,
    "ipc:process-priority-changed"
  );
  await BrowserTestUtils.withNewTab(
    GetTestWebBasedURL("dummy.html"),
    async () => {
      await processPriorityChangedPromise;
      // We can't be sure that a busy loop lasting for a specific duration
      // will use the same amount of CPU time, as that would require a core
      // to be fully available for our busy loop, which is unlikely on single
      // core hardware.
      // To be able to have a predictable amount of CPU time used, we need to
      // check using ChromeUtils.requestProcInfo how much CPU time has actually
      // been spent.
      let pid = firstBrowser.frameLoader.remoteTab.osPid;
      let initalCpuTime = await getChildCpuTime(pid);
      let afterCpuTime;
      do {
        await SpecialPowers.spawn(firstBrowser, [kBusyWaitForMs], async () => {
          let startTime = Date.now();
          while (Date.now() - startTime < 10) {
            // Burn CPU time...
          }
        });
        afterCpuTime = await getChildCpuTime(pid);
      } while (afterCpuTime - initalCpuTime < kBusyWaitForMs * kNS_PER_MS);
      cpuTimeSpentOnBackgroundTab = Math.floor(
        (afterCpuTime - initalCpuTime) / kNS_PER_MS
      );
    }
  );

  let beforeProcInfo = await ChromeUtils.requestProcInfo();
  await Services.fog.testFlushAllChildren();

  let cpuTimeByType = {},
    gpuTimeByType = {};
  for (let label of kGleanProcessTypeLabels) {
    cpuTimeByType[label] =
      Glean.power.cpuTimePerProcessTypeMs[label].testGetValue();
    gpuTimeByType[label] =
      Glean.power.gpuTimePerProcessTypeMs[label].testGetValue();
  }
  let totalCpuTime = Glean.power.totalCpuTimeMs.testGetValue();
  let totalGpuTime = Glean.power.totalGpuTimeMs.testGetValue();

  let afterProcInfo = await ChromeUtils.requestProcInfo();

  info("CPU time from ProcInfo before calling testFlushAllChildren:");
  printProcInfo(beforeProcInfo);

  info("CPU time for each label:");
  let totalCpuTimeByType = 0;
  for (let label of kGleanProcessTypeLabels) {
    totalCpuTimeByType += cpuTimeByType[label] ?? 0;
    info(`  ${label} = ${cpuTimeByType[label]}`);
  }

  info("CPU time from ProcInfo after calling testFlushAllChildren:");
  printProcInfo(afterProcInfo);

  Assert.equal(
    totalCpuTimeByType,
    totalCpuTime,
    "The sum of CPU time used by all process types should match totalCpuTimeMs"
  );

  // In infra the parent process time will be reported as parent.inactive,
  // but when running the test locally the user might move the mouse a bit.
  let parentTime =
    (cpuTimeByType["parent.active"] || 0) +
    (cpuTimeByType["parent.inactive"] || 0);
  Assert.greaterOrEqual(
    parentTime,
    Math.floor(beforeProcInfo.cpuTime / kNS_PER_MS),
    "reported parent cpu time should be at least what the first requestProcInfo returned"
  );
  Assert.lessOrEqual(
    parentTime,
    Math.ceil(afterProcInfo.cpuTime / kNS_PER_MS),
    "reported parent cpu time should be at most what the second requestProcInfo returned"
  );

  kGleanProcessTypeLabels
    .filter(label => label.startsWith("parent.") && label.includes(".playing-"))
    .forEach(label => {
      Assert.strictEqual(
        cpuTimeByType[label],
        null,
        `no media was played so the CPU time for ${label} should be null`
      );
    });

  if (beforeProcInfo.children.some(p => p.type == "preallocated")) {
    Assert.greaterOrEqual(
      cpuTimeByType.prealloc,
      beforeProcInfo.children.reduce(
        (time, p) =>
          time +
          (p.type == "preallocated" ? Math.floor(p.cpuTime / kNS_PER_MS) : 0),
        0
      ),
      "reported cpu time for preallocated content processes should be at least the sum of what the first requestProcInfo returned."
    );
    // We can't compare with the values returned by the second requestProcInfo
    // call because one preallocated content processes has been turned into
    // a normal content process when we opened a tab.
  } else {
    info(
      "no preallocated content process existed when we started our test, but some might have existed before"
    );
  }

  if (beforeProcInfo.children.some(p => p.type == "privilegedabout")) {
    Assert.greaterOrEqual(
      cpuTimeByType.privilegedabout,
      1,
      "we used some CPU time in a foreground tab, but don't know how much as the process might have started as preallocated"
    );
  }

  for (let label of [
    "rdd",
    "socket",
    "extension",
    "gpu",
    "gmplugin",
    "utility",
  ]) {
    if (!kGleanProcessTypeLabels.includes(label)) {
      Assert.ok(
        false,
        `coding error in the test, ${label} isn't part of ${kGleanProcessTypeLabels.join(
          ", "
        )}`
      );
    }
    if (beforeProcInfo.children.some(p => p.type == label)) {
      function getCpuTime(procInfo) {
        return (
          procInfo.children.find(p => p.type == label).cpuTime / kNS_PER_MS
        );
      }
      let beforeCpuTime = Math.floor(getCpuTime(beforeProcInfo));
      let afterCpuTime = Math.ceil(getCpuTime(afterProcInfo));
      let cpuTime = cpuTimeByType[label];
      if (afterCpuTime == 0) {
        Assert.equal(
          cpuTime,
          null,
          "The " + label + " process used less than 1ms of CPU time."
        );
      } else if (beforeCpuTime == 0 && cpuTime === null) {
        info(
          "The " +
            label +
            " process might have used used less than 1ms of CPU time."
        );
      } else {
        Assert.greaterOrEqual(
          cpuTime,
          beforeCpuTime,
          "reported cpu time for " +
            label +
            " process should be at least what the first requestProcInfo returned."
        );
        Assert.lessOrEqual(
          cpuTime,
          afterCpuTime,
          "reported cpu time for " +
            label +
            " process should be at most what the second requestProcInfo returned."
        );
      }
    } else {
      info(
        "no " +
          label +
          " process existed when we started our test, but some might have existed before"
      );
    }
  }

  Assert.greaterOrEqual(
    cpuTimeByType["web.background"],
    cpuTimeSpentOnBackgroundTab,
    "web.background should be at least the time we spent."
  );

  Assert.greaterOrEqual(
    cpuTimeByType["web.foreground"],
    1,
    "we used some CPU time in a foreground tab, but don't know how much"
  );

  // We only have web.background-perceivable CPU time if a muted video was
  // played in a background tab.
  Assert.strictEqual(
    cpuTimeByType["web.background-perceivable"],
    null,
    "CPU time should only be recorded in the web.background-perceivable label"
  );

  // __other__ should be null, if it is not, we have a missing label in the metrics.yaml file.
  Assert.strictEqual(
    cpuTimeByType.__other__,
    null,
    "no CPU time should be recorded in the __other__ label"
  );

  info("GPU time for each label:");
  let totalGpuTimeByType = undefined;
  for (let label of kGleanProcessTypeLabels) {
    if (gpuTimeByType[label] !== null) {
      totalGpuTimeByType = (totalGpuTimeByType || 0) + gpuTimeByType[label];
    }
    info(`  ${label} = ${gpuTimeByType[label]}`);
  }

  Assert.equal(
    totalGpuTimeByType,
    totalGpuTime,
    "The sum of GPU time used by all process types should match totalGpuTimeMs"
  );

  // __other__ should be null, if it is not, we have a missing label in the metrics.yaml file.
  Assert.strictEqual(
    gpuTimeByType.__other__,
    null,
    "no GPU time should be recorded in the __other__ label"
  );

  // Now test per-thread CPU time.
  // We don't test parentActive as the user is not marked active on infra.
  let processTypes = [
    "parentInactive",
    "contentBackground",
    "contentForeground",
  ];
  if (beforeProcInfo.children.some(p => p.type == "gpu")) {
    processTypes.push("gpuProcess");
  }
  // The list of accepted labels is not accessible to the JS code, so test only the main thread.
  const kThreadName = "geckomain";
  if (AppConstants.NIGHTLY_BUILD) {
    for (let processType of processTypes) {
      Assert.greater(
        Glean.powerCpuMsPerThread[processType][kThreadName].testGetValue(),
        0,
        `some CPU time should have been recorded for the ${processType} main thread`
      );
      Assert.greater(
        Glean.powerWakeupsPerThread[processType][kThreadName].testGetValue(),
        0,
        `some thread wake ups should have been recorded for the ${processType} main thread`
      );
    }
  } else {
    // We are not recording per thread CPU use outside of the Nightly channel.
    for (let processType of processTypes) {
      Assert.equal(
        Glean.powerCpuMsPerThread[processType][kThreadName].testGetValue(),
        null,
        `no CPU time should have been recorded for the ${processType} main thread`
      );
      Assert.equal(
        Glean.powerWakeupsPerThread[processType][kThreadName].testGetValue(),
        null,
        `no thread wake ups should have been recorded for the ${processType} main thread`
      );
    }
  }
});

add_task(async function test_tracker_power() {
  await SpecialPowers.pushPrefEnv({
    set: [
      ["privacy.trackingprotection.enabled", false],
      ["privacy.trackingprotection.annotate_channels", true],
    ],
  });
  let initialValues = [];
  for (let trackerType of [
    "ad",
    "analytics",
    "cryptomining",
    "fingerprinting",
    "social",
    "unknown",
  ]) {
    initialValues[trackerType] =
      Glean.power.cpuTimePerTrackerTypeMs[trackerType].testGetValue() || 0;
  }

  await BrowserTestUtils.withNewTab(
    GetTestWebBasedURL("dummy.html"),
    async () => {
      // Load a tracker in a subframe, as we only record CPU time used by third party trackers.
      await SpecialPowers.spawn(
        gBrowser.selectedTab.linkedBrowser,
        [
          GetTestWebBasedURL("dummy.html").replace(
            "example.org",
            "itisatracker.org"
          ),
        ],
        async frameUrl => {
          let iframe = content.document.createElement("iframe");
          iframe.setAttribute("src", frameUrl);
          await new content.Promise(resolve => {
            iframe.onload = resolve;
            content.document.body.appendChild(iframe);
          });
        }
      );
    }
  );

  await Services.fog.testFlushAllChildren();

  let unknownTrackerCPUTime =
    Glean.power.cpuTimePerTrackerTypeMs.unknown.testGetValue() || 0;
  Assert.greater(
    unknownTrackerCPUTime,
    initialValues.unknown,
    "The CPU time of unknown trackers should have increased"
  );

  for (let trackerType of [
    "ad",
    "analytics",
    "cryptomining",
    "fingerprinting",
    "social",
  ]) {
    Assert.equal(
      Glean.power.cpuTimePerTrackerTypeMs[trackerType].testGetValue() || 0,
      initialValues[trackerType],
      `no new CPU time should have been recorded for ${trackerType} trackers`
    );
  }
});
