summaryrefslogtreecommitdiffstats
path: root/tools/profiler/gecko/ProfilerParent.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /tools/profiler/gecko/ProfilerParent.cpp
parentInitial commit. (diff)
downloadfirefox-esr-upstream/115.8.0esr.tar.xz
firefox-esr-upstream/115.8.0esr.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tools/profiler/gecko/ProfilerParent.cpp')
-rw-r--r--tools/profiler/gecko/ProfilerParent.cpp1002
1 files changed, 1002 insertions, 0 deletions
diff --git a/tools/profiler/gecko/ProfilerParent.cpp b/tools/profiler/gecko/ProfilerParent.cpp
new file mode 100644
index 0000000000..83bce6d982
--- /dev/null
+++ b/tools/profiler/gecko/ProfilerParent.cpp
@@ -0,0 +1,1002 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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"
+
+#ifdef MOZ_GECKO_PROFILER
+# include "nsProfiler.h"
+# include "platform.h"
+#endif
+
+#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/RefPtr.h"
+#include "mozilla/Unused.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;
+#ifdef MOZ_GECKO_PROFILER
+ 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();
+#endif
+
+ return child;
+}
+
+#ifdef MOZ_GECKO_PROFILER
+
+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);
+
+ void HandleChunkManagerNonFinalUpdate(
+ base::ProcessId aProcessId,
+ ProfileBufferControlledChunkManager::Update&& aUpdate,
+ ProfileBufferControlledChunkManager& aParentChunkManager);
+
+ 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 */
+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;
+};
+
+static const Json::StaticString logRoot{"bufferGlobalController"};
+
+template <typename F>
+void ProfileBufferGlobalController::Log(F&& aF) {
+ ProfilingLog::Access([&](Json::Value& aLog) {
+ Json::Value& root = aLog[logRoot];
+ 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");
+
+ 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;
+ }
+
+ // 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),
+ *lockedParentChunkManagerAndPendingUpdate->mChunkManager);
+ lockedParentChunkManagerAndPendingUpdate->mPendingUpdate.Clear();
+ }
+
+ HandleChunkManagerNonFinalUpdate(
+ aProcessId, std::move(aUpdate),
+ *lockedParentChunkManagerAndPendingUpdate->mChunkManager);
+}
+
+/* static */
+bool ProfileBufferGlobalController::IsLockedOnCurrentThread() {
+ return sParentChunkManagerAndPendingUpdate.Mutex().IsLockedOnCurrentThread();
+}
+
+void ProfileBufferGlobalController::HandleChunkManagerNonFinalUpdate(
+ base::ProcessId aProcessId,
+ ProfileBufferControlledChunkManager::Update&& aUpdate,
+ ProfileBufferControlledChunkManager& aParentChunkManager) {
+ 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
+
+ std::vector<ProfileBufferControlledChunkManager::ChunkMetadata> toDestroy;
+ 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;
+ if (oldest.mProcessId == mParentProcessId) {
+ aParentChunkManager.DestroyChunksAtOrBefore(oldest.mTimeStamp);
+ } else {
+ ProfilerParentTracker::ForChild(
+ oldest.mProcessId,
+ [timestamp = oldest.mTimeStamp](ProfilerParent* profilerParent) {
+ Unused << profilerParent->SendDestroyReleasedChunksAtOrBefore(
+ timestamp);
+ });
+ }
+ 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 UniquePtr<ProfilerParentTracker> instance = nullptr;
+ if (MOZ_UNLIKELY(!instance)) {
+ if (PastShutdownPhase(ShutdownPhase::XPCOMShutdownThreads)) {
+ return nullptr;
+ }
+
+ instance = MakeUnique<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) * 8u);
+ }
+
+ 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 = 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) * 8u);
+ }
+}
+
+/* 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);
+ }
+
+ Unused << SendEnsureStarted(ipcParams);
+ RequestChunkManagerUpdate();
+ return;
+ }
+ }
+
+ Unused << SendStop();
+}
+#endif // MOZ_GECKO_PROFILER
+
+ProfilerParent::~ProfilerParent() {
+ MOZ_COUNT_DTOR(ProfilerParent);
+
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+#ifdef MOZ_GECKO_PROFILER
+ ProfilerParentTracker::StopTracking(this);
+#endif
+}
+
+#ifdef MOZ_GECKO_PROFILER
+/* 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.Data());
+ }
+ 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) {
+ Unused << 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;
+}
+
+#endif
+
+} // namespace mozilla