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

add_task(async () => {
  if (!Services.profiler.GetFeatures().includes("nativeallocations")) {
    Assert.ok(
      true,
      "Native allocations are not supported by this build, " +
        "skip run the rest of the test."
    );
    return;
  }

  Assert.ok(
    !Services.profiler.IsActive(),
    "The profiler is not currently active"
  );

  info(
    "Test that the profiler can install memory hooks and collect native allocation " +
      "information in the marker payloads."
  );
  {
    info("Start the profiler.");
    await ProfilerTestUtils.startProfiler({
      // Only instrument the main thread.
      threads: ["GeckoMain"],
      features: ["js", "nativeallocations"],
    });

    info(
      "Do some JS work for a little bit. This will increase the amount of allocations " +
        "that take place."
    );
    doWork();

    info("Get the profile data and analyze it.");
    const profile = await ProfilerTestUtils.waitSamplingAndStopAndGetProfile();

    const {
      allocationPayloads,
      unmatchedAllocations,
      logAllocationsAndDeallocations,
    } = getAllocationInformation(profile);

    Assert.greater(
      allocationPayloads.length,
      0,
      "Native allocation payloads were recorded for the parent process' main thread when " +
        "the Native Allocation feature was turned on."
    );

    if (unmatchedAllocations.length !== 0) {
      info(
        "There were unmatched allocations. Log all of the allocations and " +
          "deallocations in order to aid debugging."
      );
      logAllocationsAndDeallocations();
      ok(
        false,
        "Found a deallocation that did not have a matching allocation site. " +
          "This could happen if balanced allocations is broken, or if the the " +
          "buffer size of this test was too small, and some markers ended up " +
          "rolling off."
      );
    }

    ok(true, "All deallocation sites had matching allocations.");
  }

  info("Restart the profiler, to ensure that we get no more allocations.");
  {
    await ProfilerTestUtils.startProfiler({ features: ["js"] });
    info("Do some work again.");
    doWork();
    info("Wait for the periodic sampling.");
    const profile = await ProfilerTestUtils.waitSamplingAndStopAndGetProfile();
    const allocationPayloads = ProfilerTestUtils.getPayloadsOfType(
      profile.threads[0],
      "Native allocation"
    );

    Assert.equal(
      allocationPayloads.length,
      0,
      "No native allocations were collected when the feature was disabled."
    );
  }
});

function doWork() {
  this.n = 0;
  for (let i = 0; i < 1e5; i++) {
    this.n += Math.random();
  }
}

/**
 * Extract the allocation payloads, and find the unmatched allocations.
 */
function getAllocationInformation(profile) {
  // Get all of the allocation payloads.
  const allocationPayloads = ProfilerTestUtils.getPayloadsOfType(
    profile.threads[0],
    "Native allocation"
  );

  // Decide what is an allocation and deallocation.
  const allocations = allocationPayloads.filter(
    payload => ensureIsNumber(payload.size) >= 0
  );
  const deallocations = allocationPayloads.filter(
    payload => ensureIsNumber(payload.size) < 0
  );

  // Now determine the unmatched allocations by building a set
  const allocationSites = new Set(
    allocations.map(({ memoryAddress }) => memoryAddress)
  );

  const unmatchedAllocations = deallocations.filter(
    ({ memoryAddress }) => !allocationSites.has(memoryAddress)
  );

  // Provide a helper to log out the allocations and deallocations on failure.
  function logAllocationsAndDeallocations() {
    for (const { memoryAddress } of allocations) {
      console.log("Allocations", formatHex(memoryAddress));
      allocationSites.add(memoryAddress);
    }

    for (const { memoryAddress } of deallocations) {
      console.log("Deallocations", formatHex(memoryAddress));
    }

    for (const { memoryAddress } of unmatchedAllocations) {
      console.log("Deallocation with no allocation", formatHex(memoryAddress));
    }
  }

  return {
    allocationPayloads,
    unmatchedAllocations,
    logAllocationsAndDeallocations,
  };
}

function ensureIsNumber(value) {
  if (typeof value !== "number") {
    throw new Error(`Expected a number: ${value}`);
  }
  return value;
}

function formatHex(number) {
  return `0x${number.toString(16)}`;
}
