diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /tools/profiler/gecko/ProfilerParent.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-37a0381f8351b370577b65028ba1f6563ae23fdf.tar.xz firefox-esr-37a0381f8351b370577b65028ba1f6563ae23fdf.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.cpp | 1002 |
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 |