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

#include "ProfilerParent.h"

#include "nsProfiler.h"
#include "platform.h"

#include "GeckoProfiler.h"
#include "ProfilerControl.h"
#include "mozilla/BaseAndGeckoProfilerDetail.h"
#include "mozilla/BaseProfilerDetail.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/DataMutex.h"
#include "mozilla/IOInterposer.h"
#include "mozilla/ipc/Endpoint.h"
#include "mozilla/Maybe.h"
#include "mozilla/ProfileBufferControlledChunkManager.h"
#include "mozilla/ProfilerBufferSize.h"
#include "mozilla/RefPtr.h"
#include "nsTArray.h"
#include "nsThreadUtils.h"

#include <utility>

namespace mozilla {

using namespace ipc;

/* static */
Endpoint<PProfilerChild> ProfilerParent::CreateForProcess(
    base::ProcessId aOtherPid) {
  MOZ_RELEASE_ASSERT(NS_IsMainThread());
  Endpoint<PProfilerChild> child;
  Endpoint<PProfilerParent> parent;
  nsresult rv = PProfiler::CreateEndpoints(&parent, &child);

  if (NS_FAILED(rv)) {
    MOZ_CRASH("Failed to create top level actor for PProfiler!");
  }

  RefPtr<ProfilerParent> actor = new ProfilerParent(aOtherPid);
  if (!parent.Bind(actor)) {
    MOZ_CRASH("Failed to bind parent actor for PProfiler!");
  }

  actor->Init();

  return child;
}

class ProfilerParentTracker;

// This class is responsible for gathering updates from chunk managers in
// different process, and request for the oldest chunks to be destroyed whenever
// the given memory limit is reached.
class ProfileBufferGlobalController final {
 public:
  explicit ProfileBufferGlobalController(size_t aMaximumBytes);

  ~ProfileBufferGlobalController();

  void HandleChildChunkManagerUpdate(
      base::ProcessId aProcessId,
      ProfileBufferControlledChunkManager::Update&& aUpdate);

  static bool IsLockedOnCurrentThread();

 private:
  // Calls aF(Json::Value&).
  template <typename F>
  void Log(F&& aF);

  static void LogUpdateChunks(Json::Value& updates, base::ProcessId aProcessId,
                              const TimeStamp& aTimeStamp, int aChunkDiff);
  void LogUpdate(base::ProcessId aProcessId,
                 const ProfileBufferControlledChunkManager::Update& aUpdate);
  void LogDeletion(base::ProcessId aProcessId, const TimeStamp& aTimeStamp);

  struct ChunkToDestroy {
    base::ProcessId mProcessId;
    TimeStamp mTimeStamp;
  };

  // Collects the chunks to destroy into aToDestroy rather than destroying them,
  // so the caller can do so after releasing
  // sParentChunkManagerAndPendingUpdate; see the destruction loop in
  // HandleChildChunkManagerUpdate for why.
  void HandleChunkManagerNonFinalUpdate(
      base::ProcessId aProcessId,
      ProfileBufferControlledChunkManager::Update&& aUpdate,
      nsTArray<ChunkToDestroy>& aToDestroy);

  const size_t mMaximumBytes;

  const base::ProcessId mParentProcessId = base::GetCurrentProcId();

  struct ParentChunkManagerAndPendingUpdate {
    ProfileBufferControlledChunkManager* mChunkManager = nullptr;
    ProfileBufferControlledChunkManager::Update mPendingUpdate;
  };

  static DataMutexBase<ParentChunkManagerAndPendingUpdate,
                       baseprofiler::detail::BaseProfilerMutex>
      sParentChunkManagerAndPendingUpdate;

  size_t mUnreleasedTotalBytes = 0;

  struct PidAndBytes {
    base::ProcessId mProcessId;
    size_t mBytes;

    // For searching and sorting.
    bool operator==(base::ProcessId aSearchedProcessId) const {
      return mProcessId == aSearchedProcessId;
    }
    bool operator==(const PidAndBytes& aOther) const {
      return mProcessId == aOther.mProcessId;
    }
    bool operator<(base::ProcessId aSearchedProcessId) const {
      return mProcessId < aSearchedProcessId;
    }
    bool operator<(const PidAndBytes& aOther) const {
      return mProcessId < aOther.mProcessId;
    }
  };
  using PidAndBytesArray = nsTArray<PidAndBytes>;
  PidAndBytesArray mUnreleasedBytesByPid;

  size_t mReleasedTotalBytes = 0;

  struct TimeStampAndBytesAndPid {
    TimeStamp mTimeStamp;
    size_t mBytes;
    base::ProcessId mProcessId;

    // For searching and sorting.
    bool operator==(const TimeStampAndBytesAndPid& aOther) const {
      // Sort first by timestamps, and then by pid in rare cases with the same
      // timestamps.
      return mTimeStamp == aOther.mTimeStamp && mProcessId == aOther.mProcessId;
    }
    bool operator<(const TimeStampAndBytesAndPid& aOther) const {
      // Sort first by timestamps, and then by pid in rare cases with the same
      // timestamps.
      return mTimeStamp < aOther.mTimeStamp ||
             (MOZ_UNLIKELY(mTimeStamp == aOther.mTimeStamp) &&
              mProcessId < aOther.mProcessId);
    }
  };
  using TimeStampAndBytesAndPidArray = nsTArray<TimeStampAndBytesAndPid>;
  TimeStampAndBytesAndPidArray mReleasedChunksByTime;
};

/* static */
MOZ_RUNINIT DataMutexBase<
    ProfileBufferGlobalController::ParentChunkManagerAndPendingUpdate,
    baseprofiler::detail::BaseProfilerMutex>
    ProfileBufferGlobalController::sParentChunkManagerAndPendingUpdate{
        "ProfileBufferGlobalController::sParentChunkManagerAndPendingUpdate"};

// This singleton class tracks live ProfilerParent's (meaning there's a current
// connection with a child process).
// It also knows when the local profiler is running.
// And when both the profiler is running and at least one child is present, it
// creates a ProfileBufferGlobalController and forwards chunk updates to it.
class ProfilerParentTracker final {
 public:
  static void StartTracking(ProfilerParent* aParent);
  static void StopTracking(ProfilerParent* aParent);

  static void ProfilerStarted(uint32_t aEntries);
  static void ProfilerWillStopIfStarted();

  // Number of non-destroyed tracked ProfilerParents.
  static size_t ProfilerParentCount();

  template <typename FuncType>
  static void Enumerate(FuncType&& aIterFunc);

  template <typename FuncType>
  static void ForChild(base::ProcessId aChildPid, FuncType&& aIterFunc);

  static void ForwardChildChunkManagerUpdate(
      base::ProcessId aProcessId,
      ProfileBufferControlledChunkManager::Update&& aUpdate);

  ProfilerParentTracker();
  ~ProfilerParentTracker();

 private:
  // Get the singleton instance; Create one on the first request, unless we are
  // past XPCOMShutdownThreads, which is when it should get destroyed.
  static ProfilerParentTracker* GetInstance();

  // List of parents for currently-connected child processes.
  nsTArray<ProfilerParent*> mProfilerParents;

  // If non-0, the parent profiler is running, with this limit (in number of
  // entries.) This is needed here, because the parent profiler may start
  // running before child processes are known (e.g., startup profiling).
  uint32_t mEntries = 0;

  // When the profiler is running and there is at least one parent-child
  // connection, this is the controller that should receive chunk updates.
  Maybe<ProfileBufferGlobalController> mMaybeController;
};

template <typename F>
void ProfileBufferGlobalController::Log(F&& aF) {
  ProfilingLog::Access([&](Json::Value& aLog) {
    Json::Value& root = aLog[Json::StaticString{"bufferGlobalController"}];
    if (!root.isObject()) {
      root = Json::Value(Json::objectValue);
      root[Json::StaticString{"logBegin" TIMESTAMP_JSON_SUFFIX}] =
          ProfilingLog::Timestamp();
    }
    std::forward<F>(aF)(root);
  });
}

/* static */
void ProfileBufferGlobalController::LogUpdateChunks(Json::Value& updates,
                                                    base::ProcessId aProcessId,
                                                    const TimeStamp& aTimeStamp,
                                                    int aChunkDiff) {
  MOZ_ASSERT(updates.isArray());
  Json::Value row{Json::arrayValue};
  row.append(Json::Value{Json::UInt64(aProcessId)});
  row.append(ProfilingLog::Timestamp(aTimeStamp));
  row.append(Json::Value{Json::Int(aChunkDiff)});
  updates.append(std::move(row));
}

void ProfileBufferGlobalController::LogUpdate(
    base::ProcessId aProcessId,
    const ProfileBufferControlledChunkManager::Update& aUpdate) {
  Log([&](Json::Value& aRoot) {
    Json::Value& updates = aRoot[Json::StaticString{"updates"}];
    if (!updates.isArray()) {
      aRoot[Json::StaticString{"updatesSchema"}] =
          Json::StaticString{"0: pid, 1: chunkRelease_TSms, 3: chunkDiff"};
      updates = Json::Value{Json::arrayValue};
    }
    if (aUpdate.IsFinal()) {
      LogUpdateChunks(updates, aProcessId, TimeStamp{}, 0);
    } else if (!aUpdate.IsNotUpdate()) {
      for (const auto& chunk : aUpdate.NewlyReleasedChunksRef()) {
        LogUpdateChunks(updates, aProcessId, chunk.mDoneTimeStamp, 1);
      }
    }
  });
}

void ProfileBufferGlobalController::LogDeletion(base::ProcessId aProcessId,
                                                const TimeStamp& aTimeStamp) {
  Log([&](Json::Value& aRoot) {
    Json::Value& updates = aRoot[Json::StaticString{"updates"}];
    if (!updates.isArray()) {
      updates = Json::Value{Json::arrayValue};
    }
    LogUpdateChunks(updates, aProcessId, aTimeStamp, -1);
  });
}

ProfileBufferGlobalController::ProfileBufferGlobalController(
    size_t aMaximumBytes)
    : mMaximumBytes(aMaximumBytes) {
  MOZ_RELEASE_ASSERT(NS_IsMainThread());

  Log([](Json::Value& aRoot) {
    aRoot[Json::StaticString{"controllerCreationTime" TIMESTAMP_JSON_SUFFIX}] =
        ProfilingLog::Timestamp();
  });

  // This is the local chunk manager for this parent process, so updates can be
  // handled here.
  ProfileBufferControlledChunkManager* parentChunkManager =
      profiler_get_controlled_chunk_manager();

  if (NS_WARN_IF(!parentChunkManager)) {
    Log([](Json::Value& aRoot) {
      aRoot[Json::StaticString{"controllerCreationFailureReason"}] =
          "No parent chunk manager";
    });
    return;
  }

  {
    auto lockedParentChunkManagerAndPendingUpdate =
        sParentChunkManagerAndPendingUpdate.Lock();
    lockedParentChunkManagerAndPendingUpdate->mChunkManager =
        parentChunkManager;
  }

  parentChunkManager->SetUpdateCallback(
      [this](ProfileBufferControlledChunkManager::Update&& aUpdate) {
        MOZ_ASSERT(!aUpdate.IsNotUpdate(),
                   "Update callback should never be given a non-update");
        auto lockedParentChunkManagerAndPendingUpdate =
            sParentChunkManagerAndPendingUpdate.Lock();
        if (aUpdate.IsFinal()) {
          // Final update of the parent.
          // We cannot keep the chunk manager, and there's no point handling
          // updates anymore. Do some cleanup now, to free resources before
          // we're destroyed.
          lockedParentChunkManagerAndPendingUpdate->mChunkManager = nullptr;
          lockedParentChunkManagerAndPendingUpdate->mPendingUpdate.Clear();
          mUnreleasedTotalBytes = 0;
          mUnreleasedBytesByPid.Clear();
          mReleasedTotalBytes = 0;
          mReleasedChunksByTime.Clear();
          return;
        }
        if (!lockedParentChunkManagerAndPendingUpdate->mChunkManager) {
          // No chunk manager, ignore updates.
          return;
        }
        // Special handling of parent non-final updates:
        // These updates are coming from *this* process, and may originate from
        // scopes in any thread where any lock is held, so using other locks (to
        // e.g., dispatch tasks or send IPCs) could trigger a deadlock. Instead,
        // parent updates are stored locally and handled when the next
        // non-parent update needs handling, see HandleChildChunkManagerUpdate.
        lockedParentChunkManagerAndPendingUpdate->mPendingUpdate.Fold(
            std::move(aUpdate));
      });
}

ProfileBufferGlobalController ::~ProfileBufferGlobalController() {
  MOZ_RELEASE_ASSERT(NS_IsMainThread());
  // Extract the parent chunk manager (if still set).
  // This means any update after this will be ignored.
  ProfileBufferControlledChunkManager* parentChunkManager = []() {
    auto lockedParentChunkManagerAndPendingUpdate =
        sParentChunkManagerAndPendingUpdate.Lock();
    lockedParentChunkManagerAndPendingUpdate->mPendingUpdate.Clear();
    return std::exchange(
        lockedParentChunkManagerAndPendingUpdate->mChunkManager, nullptr);
  }();
  if (parentChunkManager) {
    // We had not received a final update yet, so the chunk manager is still
    // valid. Reset the callback in the chunk manager, this will immediately
    // invoke the callback with the final empty update; see handling above.
    parentChunkManager->SetUpdateCallback({});
  }
}

void ProfileBufferGlobalController::HandleChildChunkManagerUpdate(
    base::ProcessId aProcessId,
    ProfileBufferControlledChunkManager::Update&& aUpdate) {
  MOZ_RELEASE_ASSERT(NS_IsMainThread());

  MOZ_ASSERT(aProcessId != mParentProcessId);

  MOZ_ASSERT(!aUpdate.IsNotUpdate(),
             "HandleChildChunkManagerUpdate should not be given a non-update");

  // Chunks to destroy are collected while holding the lock and destroyed below,
  // after releasing it.
  nsTArray<ChunkToDestroy> toDestroy;
  ProfileBufferControlledChunkManager* parentChunkManager = nullptr;

  {
    auto lockedParentChunkManagerAndPendingUpdate =
        sParentChunkManagerAndPendingUpdate.Lock();
    if (!lockedParentChunkManagerAndPendingUpdate->mChunkManager) {
      // No chunk manager, ignore updates.
      return;
    }

    if (aUpdate.IsFinal()) {
      // Final update in a child process, remove all traces of that process.
      LogUpdate(aProcessId, aUpdate);
      size_t index = mUnreleasedBytesByPid.BinaryIndexOf(aProcessId);
      if (index != PidAndBytesArray::NoIndex) {
        // We already have a value for this pid.
        PidAndBytes& pidAndBytes = mUnreleasedBytesByPid[index];
        mUnreleasedTotalBytes -= pidAndBytes.mBytes;
        mUnreleasedBytesByPid.RemoveElementAt(index);
      }

      size_t released = 0;
      mReleasedChunksByTime.RemoveElementsBy(
          [&released, aProcessId](const auto& chunk) {
            const bool match = chunk.mProcessId == aProcessId;
            if (match) {
              released += chunk.mBytes;
            }
            return match;
          });
      if (released != 0) {
        mReleasedTotalBytes -= released;
      }

      // Total can only have gone down, so there's no need to check the limit.
      return;
    }

    parentChunkManager =
        lockedParentChunkManagerAndPendingUpdate->mChunkManager;

    // Non-final update in child process.

    // Before handling the child update, we may have pending updates from the
    // parent, which can be processed now since we're in an IPC callback outside
    // of any profiler-related scope.
    if (!lockedParentChunkManagerAndPendingUpdate->mPendingUpdate
             .IsNotUpdate()) {
      MOZ_ASSERT(
          !lockedParentChunkManagerAndPendingUpdate->mPendingUpdate.IsFinal());
      HandleChunkManagerNonFinalUpdate(
          mParentProcessId,
          std::move(lockedParentChunkManagerAndPendingUpdate->mPendingUpdate),
          toDestroy);
      lockedParentChunkManagerAndPendingUpdate->mPendingUpdate.Clear();
    }

    HandleChunkManagerNonFinalUpdate(aProcessId, std::move(aUpdate), toDestroy);
  }

  // Now that sParentChunkManagerAndPendingUpdate is released, perform the
  // actual chunk destructions. Doing this under the lock would close a
  // lock-order-inversion cycle: DestroyChunksAtOrBefore re-enters the parent
  // chunk manager and takes the profiler buffer lock, and
  // SendDestroyReleasedChunksAtOrBefore sends an IPC message and takes the
  // channel lock; both of those locks can be held elsewhere while a profiler
  // marker is recorded, which takes the buffer lock under which the parent
  // chunk manager update callback takes sParentChunkManagerAndPendingUpdate.
  //
  // parentChunkManager stays valid even though it was captured under the lock:
  // it is only cleared/destroyed on the main thread (~ProfileBufferGlobal
  // controller and the final-update callback), and this runs on the main thread
  // without spinning the event loop between the release above and this loop.
  for (const ChunkToDestroy& chunk : toDestroy) {
    if (chunk.mProcessId == mParentProcessId) {
      parentChunkManager->DestroyChunksAtOrBefore(chunk.mTimeStamp);
    } else {
      ProfilerParentTracker::ForChild(
          chunk.mProcessId,
          [timestamp = chunk.mTimeStamp](ProfilerParent* profilerParent) {
            (void)profilerParent->SendDestroyReleasedChunksAtOrBefore(
                timestamp);
          });
    }
  }
}

/* static */
bool ProfileBufferGlobalController::IsLockedOnCurrentThread() {
  return sParentChunkManagerAndPendingUpdate.Mutex().IsLockedOnCurrentThread();
}

void ProfileBufferGlobalController::HandleChunkManagerNonFinalUpdate(
    base::ProcessId aProcessId,
    ProfileBufferControlledChunkManager::Update&& aUpdate,
    nsTArray<ChunkToDestroy>& aToDestroy) {
  MOZ_ASSERT(!aUpdate.IsFinal());
  LogUpdate(aProcessId, aUpdate);

  size_t index = mUnreleasedBytesByPid.BinaryIndexOf(aProcessId);
  if (index != PidAndBytesArray::NoIndex) {
    // We already have a value for this pid.
    PidAndBytes& pidAndBytes = mUnreleasedBytesByPid[index];
    mUnreleasedTotalBytes =
        mUnreleasedTotalBytes - pidAndBytes.mBytes + aUpdate.UnreleasedBytes();
    pidAndBytes.mBytes = aUpdate.UnreleasedBytes();
  } else {
    // New pid.
    mUnreleasedBytesByPid.InsertElementSorted(
        PidAndBytes{aProcessId, aUpdate.UnreleasedBytes()});
    mUnreleasedTotalBytes += aUpdate.UnreleasedBytes();
  }

  size_t destroyedReleased = 0;
  if (!aUpdate.OldestDoneTimeStamp().IsNull()) {
    size_t i = 0;
    for (; i < mReleasedChunksByTime.Length(); ++i) {
      if (mReleasedChunksByTime[i].mTimeStamp >=
          aUpdate.OldestDoneTimeStamp()) {
        break;
      }
    }
    // Here, i is the index of the first item that's at or after
    // aUpdate.mOldestDoneTimeStamp, so chunks from aProcessId before that have
    // been destroyed.
    while (i != 0) {
      --i;
      const TimeStampAndBytesAndPid& item = mReleasedChunksByTime[i];
      if (item.mProcessId == aProcessId) {
        destroyedReleased += item.mBytes;
        mReleasedChunksByTime.RemoveElementAt(i);
      }
    }
  }

  size_t newlyReleased = 0;
  for (const ProfileBufferControlledChunkManager::ChunkMetadata& chunk :
       aUpdate.NewlyReleasedChunksRef()) {
    newlyReleased += chunk.mBufferBytes;
    mReleasedChunksByTime.InsertElementSorted(TimeStampAndBytesAndPid{
        chunk.mDoneTimeStamp, chunk.mBufferBytes, aProcessId});
  }

  mReleasedTotalBytes = mReleasedTotalBytes - destroyedReleased + newlyReleased;

#ifdef DEBUG
  size_t totalReleased = 0;
  for (const TimeStampAndBytesAndPid& item : mReleasedChunksByTime) {
    totalReleased += item.mBytes;
  }
  MOZ_ASSERT(mReleasedTotalBytes == totalReleased);
#endif  // DEBUG

  while (mUnreleasedTotalBytes + mReleasedTotalBytes > mMaximumBytes &&
         !mReleasedChunksByTime.IsEmpty()) {
    // We have reached the global memory limit, and there *are* released chunks
    // that can be destroyed. Start with the first one, which is the oldest.
    const TimeStampAndBytesAndPid& oldest = mReleasedChunksByTime[0];
    LogDeletion(oldest.mProcessId, oldest.mTimeStamp);
    mReleasedTotalBytes -= oldest.mBytes;
    aToDestroy.AppendElement(
        ChunkToDestroy{oldest.mProcessId, oldest.mTimeStamp});
    mReleasedChunksByTime.RemoveElementAt(0);
  }
}

/* static */
ProfilerParentTracker* ProfilerParentTracker::GetInstance() {
  MOZ_RELEASE_ASSERT(NS_IsMainThread());

  // The main instance pointer, it will be initialized at most once, before
  // XPCOMShutdownThreads.
  static StaticAutoPtr<ProfilerParentTracker> instance;
  if (MOZ_UNLIKELY(!instance)) {
    if (PastShutdownPhase(ShutdownPhase::XPCOMShutdownThreads)) {
      return nullptr;
    }

    instance = new ProfilerParentTracker();

    // The tracker should get destroyed before threads are shutdown, because its
    // destruction closes extant channels, which could trigger promise
    // rejections that need to be dispatched to other threads.
    ClearOnShutdown(&instance, ShutdownPhase::XPCOMShutdownThreads);
  }

  return instance.get();
}

/* static */
void ProfilerParentTracker::StartTracking(ProfilerParent* aProfilerParent) {
  ProfilerParentTracker* tracker = GetInstance();
  if (!tracker) {
    return;
  }

  if (tracker->mMaybeController.isNothing() && tracker->mEntries != 0) {
    // There is no controller yet, but the profiler has started.
    // Since we're adding a ProfilerParent, it's a good time to start
    // controlling the global memory usage of the profiler.
    // (And this helps delay the Controller startup, because the parent profiler
    // can start *very* early in the process, when some resources like threads
    // are not ready yet.)
    tracker->mMaybeController.emplace(size_t(tracker->mEntries) *
                                      scBytesPerEntry);
  }

  tracker->mProfilerParents.AppendElement(aProfilerParent);
}

/* static */
void ProfilerParentTracker::StopTracking(ProfilerParent* aParent) {
  ProfilerParentTracker* tracker = GetInstance();
  if (!tracker) {
    return;
  }

  tracker->mProfilerParents.RemoveElement(aParent);
}

/* static */
void ProfilerParentTracker::ProfilerStarted(uint32_t aEntries) {
  ProfilerParentTracker* tracker = GetInstance();
  if (!tracker) {
    return;
  }

  tracker->mEntries = ClampToAllowedEntries(aEntries);

  if (tracker->mMaybeController.isNothing() &&
      !tracker->mProfilerParents.IsEmpty()) {
    // We are already tracking child processes, so it's a good time to start
    // controlling the global memory usage of the profiler.
    tracker->mMaybeController.emplace(size_t(tracker->mEntries) *
                                      scBytesPerEntry);
  }
}

/* static */
void ProfilerParentTracker::ProfilerWillStopIfStarted() {
  ProfilerParentTracker* tracker = GetInstance();
  if (!tracker) {
    return;
  }

  tracker->mEntries = 0;
  tracker->mMaybeController = Nothing{};
}

/* static */
size_t ProfilerParentTracker::ProfilerParentCount() {
  size_t count = 0;
  ProfilerParentTracker* tracker = GetInstance();
  if (tracker) {
    for (ProfilerParent* profilerParent : tracker->mProfilerParents) {
      if (!profilerParent->mDestroyed) {
        ++count;
      }
    }
  }
  return count;
}

template <typename FuncType>
/* static */
void ProfilerParentTracker::Enumerate(FuncType&& aIterFunc) {
  ProfilerParentTracker* tracker = GetInstance();
  if (!tracker) {
    return;
  }

  for (ProfilerParent* profilerParent : tracker->mProfilerParents) {
    if (!profilerParent->mDestroyed) {
      aIterFunc(profilerParent);
    }
  }
}

template <typename FuncType>
/* static */
void ProfilerParentTracker::ForChild(base::ProcessId aChildPid,
                                     FuncType&& aIterFunc) {
  ProfilerParentTracker* tracker = GetInstance();
  if (!tracker) {
    return;
  }

  for (ProfilerParent* profilerParent : tracker->mProfilerParents) {
    if (profilerParent->mChildPid == aChildPid) {
      if (!profilerParent->mDestroyed) {
        std::forward<FuncType>(aIterFunc)(profilerParent);
      }
      return;
    }
  }
}

/* static */
void ProfilerParentTracker::ForwardChildChunkManagerUpdate(
    base::ProcessId aProcessId,
    ProfileBufferControlledChunkManager::Update&& aUpdate) {
  ProfilerParentTracker* tracker = GetInstance();
  if (!tracker || tracker->mMaybeController.isNothing()) {
    return;
  }

  MOZ_ASSERT(!aUpdate.IsNotUpdate(),
             "No process should ever send a non-update");
  tracker->mMaybeController->HandleChildChunkManagerUpdate(aProcessId,
                                                           std::move(aUpdate));
}

ProfilerParentTracker::ProfilerParentTracker() {
  MOZ_RELEASE_ASSERT(NS_IsMainThread());
  MOZ_COUNT_CTOR(ProfilerParentTracker);
}

ProfilerParentTracker::~ProfilerParentTracker() {
  // This destructor should only be called on the main thread.
  MOZ_RELEASE_ASSERT(NS_IsMainThread() ||
                     // OR we're not on the main thread (including if we are
                     // past the end of `main()`), which is fine *if* there are
                     // no ProfilerParent's still registered, in which case
                     // nothing else will happen in this destructor anyway.
                     // See bug 1713971 for more information.
                     mProfilerParents.IsEmpty());
  MOZ_COUNT_DTOR(ProfilerParentTracker);

  // Close the channels of any profiler parents that haven't been destroyed.
  for (ProfilerParent* profilerParent : mProfilerParents.Clone()) {
    if (!profilerParent->mDestroyed) {
      // Keep the object alive until the call to Close() has completed.
      // Close() will trigger a call to DeallocPProfilerParent.
      RefPtr<ProfilerParent> actor = profilerParent;
      actor->Close();
    }
  }
}

ProfilerParent::ProfilerParent(base::ProcessId aChildPid)
    : mChildPid(aChildPid), mDestroyed(false) {
  MOZ_COUNT_CTOR(ProfilerParent);

  MOZ_RELEASE_ASSERT(NS_IsMainThread());
}

void ProfilerParent::Init() {
  MOZ_RELEASE_ASSERT(NS_IsMainThread());

  ProfilerParentTracker::StartTracking(this);

  // We propagated the profiler state from the parent process to the child
  // process through MOZ_PROFILER_STARTUP* environment variables.
  // However, the profiler state might have changed in this process since then,
  // and now that an active communication channel has been established with the
  // child process, it's a good time to sync up the two profilers again.

  int entries = 0;
  Maybe<double> duration = Nothing();
  double interval = 0;
  mozilla::Vector<const char*> filters;
  uint32_t features;
  uint64_t activeTabID;
  profiler_get_start_params(&entries, &duration, &interval, &features, &filters,
                            &activeTabID);

  if (entries != 0) {
    ProfilerInitParams ipcParams;
    ipcParams.enabled() = true;
    ipcParams.entries() = entries;
    ipcParams.duration() = duration;
    ipcParams.interval() = interval;
    ipcParams.features() = features;
    ipcParams.activeTabID() = activeTabID;

    // If the filters exclude our pid, make sure it's stopped, otherwise
    // continue with starting it.
    if (!profiler::detail::FiltersExcludePid(
            filters, ProfilerProcessId::FromNumber(mChildPid))) {
      ipcParams.filters().SetCapacity(filters.length());
      for (const char* filter : filters) {
        ipcParams.filters().AppendElement(filter);
      }

      (void)SendEnsureStarted(ipcParams);
      RequestChunkManagerUpdate();
      return;
    }
  }

  (void)SendStop();
}

ProfilerParent::~ProfilerParent() {
  MOZ_COUNT_DTOR(ProfilerParent);

  MOZ_RELEASE_ASSERT(NS_IsMainThread());
  ProfilerParentTracker::StopTracking(this);
}

/* static */
nsTArray<ProfilerParent::SingleProcessProfilePromiseAndChildPid>
ProfilerParent::GatherProfiles() {
  nsTArray<SingleProcessProfilePromiseAndChildPid> results;
  if (!NS_IsMainThread()) {
    return results;
  }

  results.SetCapacity(ProfilerParentTracker::ProfilerParentCount());
  ProfilerParentTracker::Enumerate([&](ProfilerParent* profilerParent) {
    results.AppendElement(SingleProcessProfilePromiseAndChildPid{
        profilerParent->SendGatherProfile(), profilerParent->mChildPid});
  });
  return results;
}

/* static */
RefPtr<ProfilerParent::SingleProcessProgressPromise>
ProfilerParent::RequestGatherProfileProgress(base::ProcessId aChildPid) {
  RefPtr<SingleProcessProgressPromise> promise;
  ProfilerParentTracker::ForChild(
      aChildPid, [&promise](ProfilerParent* profilerParent) {
        promise = profilerParent->SendGetGatherProfileProgress();
      });
  return promise;
}

// Magic value for ProfileBufferChunkManagerUpdate::unreleasedBytes meaning
// that this is a final update from a child.
constexpr static uint64_t scUpdateUnreleasedBytesFINAL = uint64_t(-1);

/* static */
ProfileBufferChunkManagerUpdate ProfilerParent::MakeFinalUpdate() {
  return ProfileBufferChunkManagerUpdate{
      uint64_t(scUpdateUnreleasedBytesFINAL), 0, TimeStamp{},
      nsTArray<ProfileBufferChunkMetadata>{}};
}

/* static */
bool ProfilerParent::IsLockedOnCurrentThread() {
  return ProfileBufferGlobalController::IsLockedOnCurrentThread();
}

void ProfilerParent::RequestChunkManagerUpdate() {
  if (mDestroyed) {
    return;
  }

  RefPtr<AwaitNextChunkManagerUpdatePromise> updatePromise =
      SendAwaitNextChunkManagerUpdate();
  updatePromise->Then(
      GetMainThreadSerialEventTarget(), __func__,
      [self = RefPtr<ProfilerParent>(this)](
          const ProfileBufferChunkManagerUpdate& aUpdate) {
        if (aUpdate.unreleasedBytes() == scUpdateUnreleasedBytesFINAL) {
          // Special value meaning it's the final update from that child.
          ProfilerParentTracker::ForwardChildChunkManagerUpdate(
              self->mChildPid,
              ProfileBufferControlledChunkManager::Update(nullptr));
        } else {
          // Not the final update, translate it.
          std::vector<ProfileBufferControlledChunkManager::ChunkMetadata>
              chunks;
          if (!aUpdate.newlyReleasedChunks().IsEmpty()) {
            chunks.reserve(aUpdate.newlyReleasedChunks().Length());
            for (const ProfileBufferChunkMetadata& chunk :
                 aUpdate.newlyReleasedChunks()) {
              chunks.emplace_back(chunk.doneTimeStamp(), chunk.bufferBytes());
            }
          }
          // Let the tracker handle it.
          ProfilerParentTracker::ForwardChildChunkManagerUpdate(
              self->mChildPid,
              ProfileBufferControlledChunkManager::Update(
                  aUpdate.unreleasedBytes(), aUpdate.releasedBytes(),
                  aUpdate.oldestDoneTimeStamp(), std::move(chunks)));
          // This was not a final update, so start a new request.
          self->RequestChunkManagerUpdate();
        }
      },
      [self = RefPtr<ProfilerParent>(this)](
          mozilla::ipc::ResponseRejectReason aReason) {
        // Rejection could be for a number of reasons, assume the child will
        // not respond anymore, so we pretend we received a final update.
        ProfilerParentTracker::ForwardChildChunkManagerUpdate(
            self->mChildPid,
            ProfileBufferControlledChunkManager::Update(nullptr));
      });
}

// Ref-counted class that resolves a promise on destruction.
// Usage:
// RefPtr<GenericPromise> f() {
//   return PromiseResolverOnDestruction::RunTask(
//     [](RefPtr<PromiseResolverOnDestruction> aPromiseResolver){
//       // Give *copies* of aPromiseResolver to asynchronous sub-tasks, the
//       // last remaining RefPtr destruction will resolve the promise.
//     });
// }
class PromiseResolverOnDestruction {
 public:
  NS_INLINE_DECL_REFCOUNTING(PromiseResolverOnDestruction)

  template <typename TaskFunction>
  static RefPtr<GenericPromise> RunTask(TaskFunction&& aTaskFunction) {
    RefPtr<PromiseResolverOnDestruction> promiseResolver =
        new PromiseResolverOnDestruction();
    RefPtr<GenericPromise> promise =
        promiseResolver->mPromiseHolder.Ensure(__func__);
    std::forward<TaskFunction>(aTaskFunction)(std::move(promiseResolver));
    return promise;
  }

 private:
  PromiseResolverOnDestruction() = default;

  ~PromiseResolverOnDestruction() {
    mPromiseHolder.ResolveIfExists(/* unused */ true, __func__);
  }

  MozPromiseHolder<GenericPromise> mPromiseHolder;
};

// Given a ProfilerParentSendFunction: (ProfilerParent*) -> some MozPromise,
// run the function on all live ProfilerParents and return a GenericPromise, and
// when their promise gets resolve, resolve our Generic promise.
template <typename ProfilerParentSendFunction>
static RefPtr<GenericPromise> SendAndConvertPromise(
    ProfilerParentSendFunction&& aProfilerParentSendFunction) {
  if (!NS_IsMainThread()) {
    return GenericPromise::CreateAndResolve(/* unused */ true, __func__);
  }

  return PromiseResolverOnDestruction::RunTask(
      [&](RefPtr<PromiseResolverOnDestruction> aPromiseResolver) {
        ProfilerParentTracker::Enumerate([&](ProfilerParent* profilerParent) {
          std::forward<ProfilerParentSendFunction>(aProfilerParentSendFunction)(
              profilerParent)
              ->Then(GetMainThreadSerialEventTarget(), __func__,
                     [aPromiseResolver](
                         typename std::remove_reference_t<
                             decltype(*std::forward<ProfilerParentSendFunction>(
                                 aProfilerParentSendFunction)(
                                 profilerParent))>::ResolveOrRejectValue&&) {
                       // Whatever the resolution/rejection is, do nothing.
                       // The lambda aPromiseResolver ref-count will decrease.
                     });
        });
      });
}

/* static */
RefPtr<GenericPromise> ProfilerParent::ProfilerStarted(
    nsIProfilerStartParams* aParams) {
  if (!NS_IsMainThread()) {
    return GenericPromise::CreateAndResolve(/* unused */ true, __func__);
  }

  ProfilerInitParams ipcParams;
  double duration;
  ipcParams.enabled() = true;
  aParams->GetEntries(&ipcParams.entries());
  aParams->GetDuration(&duration);
  if (duration > 0.0) {
    ipcParams.duration() = Some(duration);
  } else {
    ipcParams.duration() = Nothing();
  }
  aParams->GetInterval(&ipcParams.interval());
  aParams->GetFeatures(&ipcParams.features());
  ipcParams.filters() = aParams->GetFilters().Clone();
  // We need filters as a Span<const char*> to test pids in the lambda below.
  auto filtersCStrings = nsTArray<const char*>{aParams->GetFilters().Length()};
  for (const auto& filter : aParams->GetFilters()) {
    filtersCStrings.AppendElement(filter.get());
  }
  aParams->GetActiveTabID(&ipcParams.activeTabID());

  ProfilerParentTracker::ProfilerStarted(ipcParams.entries());

  return SendAndConvertPromise([&](ProfilerParent* profilerParent) {
    if (profiler::detail::FiltersExcludePid(
            filtersCStrings,
            ProfilerProcessId::FromNumber(profilerParent->mChildPid))) {
      // This pid is excluded, don't start the profiler at all.
      return PProfilerParent::StartPromise::CreateAndResolve(/* unused */ true,
                                                             __func__);
    }
    auto promise = profilerParent->SendStart(ipcParams);
    profilerParent->RequestChunkManagerUpdate();
    return promise;
  });
}

/* static */
void ProfilerParent::ProfilerWillStopIfStarted() {
  if (!NS_IsMainThread()) {
    return;
  }

  ProfilerParentTracker::ProfilerWillStopIfStarted();
}

/* static */
RefPtr<GenericPromise> ProfilerParent::ProfilerStopped() {
  return SendAndConvertPromise([](ProfilerParent* profilerParent) {
    return profilerParent->SendStop();
  });
}

/* static */
RefPtr<GenericPromise> ProfilerParent::ProfilerPaused() {
  return SendAndConvertPromise([](ProfilerParent* profilerParent) {
    return profilerParent->SendPause();
  });
}

/* static */
RefPtr<GenericPromise> ProfilerParent::ProfilerResumed() {
  return SendAndConvertPromise([](ProfilerParent* profilerParent) {
    return profilerParent->SendResume();
  });
}

/* static */
RefPtr<GenericPromise> ProfilerParent::ProfilerPausedSampling() {
  return SendAndConvertPromise([](ProfilerParent* profilerParent) {
    return profilerParent->SendPauseSampling();
  });
}

/* static */
RefPtr<GenericPromise> ProfilerParent::ProfilerResumedSampling() {
  return SendAndConvertPromise([](ProfilerParent* profilerParent) {
    return profilerParent->SendResumeSampling();
  });
}

/* static */
void ProfilerParent::ClearAllPages() {
  if (!NS_IsMainThread()) {
    return;
  }

  ProfilerParentTracker::Enumerate([](ProfilerParent* profilerParent) {
    (void)profilerParent->SendClearAllPages();
  });
}

/* static */
RefPtr<GenericPromise> ProfilerParent::WaitOnePeriodicSampling() {
  return SendAndConvertPromise([](ProfilerParent* profilerParent) {
    return profilerParent->SendWaitOnePeriodicSampling();
  });
}

void ProfilerParent::ActorDestroy(ActorDestroyReason aActorDestroyReason) {
  MOZ_RELEASE_ASSERT(NS_IsMainThread());
  mDestroyed = true;
}

}  // namespace mozilla
