summaryrefslogtreecommitdiffstats
path: root/tools/profiler/gecko
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--tools/profiler/gecko/ChildProfilerController.cpp170
-rw-r--r--tools/profiler/gecko/PProfiler.ipdl44
-rw-r--r--tools/profiler/gecko/ProfilerChild.cpp565
-rw-r--r--tools/profiler/gecko/ProfilerIOInterposeObserver.cpp216
-rw-r--r--tools/profiler/gecko/ProfilerIOInterposeObserver.h32
-rw-r--r--tools/profiler/gecko/ProfilerParent.cpp1002
-rw-r--r--tools/profiler/gecko/ProfilerTypes.ipdlh43
-rw-r--r--tools/profiler/gecko/components.conf17
-rw-r--r--tools/profiler/gecko/nsIProfiler.idl208
-rw-r--r--tools/profiler/gecko/nsProfiler.cpp1487
-rw-r--r--tools/profiler/gecko/nsProfiler.h117
-rw-r--r--tools/profiler/gecko/nsProfilerCIID.h16
-rw-r--r--tools/profiler/gecko/nsProfilerStartParams.cpp65
-rw-r--r--tools/profiler/gecko/nsProfilerStartParams.h36
14 files changed, 4018 insertions, 0 deletions
diff --git a/tools/profiler/gecko/ChildProfilerController.cpp b/tools/profiler/gecko/ChildProfilerController.cpp
new file mode 100644
index 0000000000..f51cb9437d
--- /dev/null
+++ b/tools/profiler/gecko/ChildProfilerController.cpp
@@ -0,0 +1,170 @@
+/* -*- 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 "ChildProfilerController.h"
+
+#include "ProfilerChild.h"
+
+#include "mozilla/ProfilerState.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "nsExceptionHandler.h"
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+
+using namespace mozilla::ipc;
+
+namespace mozilla {
+
+/* static */
+already_AddRefed<ChildProfilerController> ChildProfilerController::Create(
+ mozilla::ipc::Endpoint<PProfilerChild>&& aEndpoint) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ RefPtr<ChildProfilerController> cpc = new ChildProfilerController();
+ cpc->Init(std::move(aEndpoint));
+ return cpc.forget();
+}
+
+ChildProfilerController::ChildProfilerController()
+ : mThread(nullptr, "ChildProfilerController::mThread") {
+ MOZ_COUNT_CTOR(ChildProfilerController);
+}
+
+void ChildProfilerController::Init(Endpoint<PProfilerChild>&& aEndpoint) {
+ RefPtr<nsIThread> newProfilerChildThread;
+ if (NS_SUCCEEDED(NS_NewNamedThread("ProfilerChild",
+ getter_AddRefs(newProfilerChildThread)))) {
+ {
+ auto lock = mThread.Lock();
+ RefPtr<nsIThread>& lockedmThread = lock.ref();
+ MOZ_ASSERT(!lockedmThread, "There is already a ProfilerChild thread");
+ // Copy ref'd ptr into mThread. Don't move/swap, so that
+ // newProfilerChildThread can be used below.
+ lockedmThread = newProfilerChildThread;
+ }
+ // Now that mThread has been set, run SetupProfilerChild on the thread.
+ newProfilerChildThread->Dispatch(
+ NewRunnableMethod<Endpoint<PProfilerChild>&&>(
+ "ChildProfilerController::SetupProfilerChild", this,
+ &ChildProfilerController::SetupProfilerChild, std::move(aEndpoint)),
+ NS_DISPATCH_NORMAL);
+ }
+}
+
+ProfileAndAdditionalInformation
+ChildProfilerController::GrabShutdownProfileAndShutdown() {
+ ProfileAndAdditionalInformation profileAndAdditionalInformation;
+ ShutdownAndMaybeGrabShutdownProfileFirst(&profileAndAdditionalInformation);
+ return profileAndAdditionalInformation;
+}
+
+void ChildProfilerController::Shutdown() {
+ ShutdownAndMaybeGrabShutdownProfileFirst(nullptr);
+}
+
+void ChildProfilerController::ShutdownAndMaybeGrabShutdownProfileFirst(
+ ProfileAndAdditionalInformation* aOutShutdownProfileInformation) {
+ // First, get the owning reference out of mThread, so it cannot be used in
+ // ChildProfilerController after this (including re-entrantly during the
+ // profilerChildThread->Shutdown() inner event loop below).
+ RefPtr<nsIThread> profilerChildThread;
+ {
+ auto lock = mThread.Lock();
+ RefPtr<nsIThread>& lockedmThread = lock.ref();
+ lockedmThread.swap(profilerChildThread);
+ }
+ if (profilerChildThread) {
+ if (profiler_is_active()) {
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::ProfilerChildShutdownPhase,
+ "Profiling - Dispatching ShutdownProfilerChild"_ns);
+ profilerChildThread->Dispatch(
+ NewRunnableMethod<ProfileAndAdditionalInformation*>(
+ "ChildProfilerController::ShutdownProfilerChild", this,
+ &ChildProfilerController::ShutdownProfilerChild,
+ aOutShutdownProfileInformation),
+ NS_DISPATCH_NORMAL);
+ // Shut down the thread. This call will spin until all runnables
+ // (including the ShutdownProfilerChild runnable) have been processed.
+ profilerChildThread->Shutdown();
+ } else {
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::ProfilerChildShutdownPhase,
+ "Not profiling - Running ShutdownProfilerChild"_ns);
+ // If we're not profiling, this operation will be very quick, so it can be
+ // done synchronously. This avoids having to manually shutdown the thread,
+ // which runs a risky inner event loop, see bug 1613798.
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "ChildProfilerController::ShutdownProfilerChild SYNC"_ns,
+ profilerChildThread,
+ NewRunnableMethod<ProfileAndAdditionalInformation*>(
+ "ChildProfilerController::ShutdownProfilerChild SYNC", this,
+ &ChildProfilerController::ShutdownProfilerChild, nullptr));
+ }
+ // At this point, `profilerChildThread` should be the last reference to the
+ // thread, so it will now get destroyed.
+ }
+}
+
+ChildProfilerController::~ChildProfilerController() {
+ MOZ_COUNT_DTOR(ChildProfilerController);
+
+#ifdef DEBUG
+ {
+ auto lock = mThread.Lock();
+ RefPtr<nsIThread>& lockedmThread = lock.ref();
+ MOZ_ASSERT(
+ !lockedmThread,
+ "Please call Shutdown before destroying ChildProfilerController");
+ }
+#endif
+ MOZ_ASSERT(!mProfilerChild);
+}
+
+void ChildProfilerController::SetupProfilerChild(
+ Endpoint<PProfilerChild>&& aEndpoint) {
+ {
+ auto lock = mThread.Lock();
+ RefPtr<nsIThread>& lockedmThread = lock.ref();
+ // We should be on the ProfilerChild thread. In rare cases, we could already
+ // be in shutdown, in which case mThread is null; we still need to continue,
+ // so that ShutdownProfilerChild can work on a valid mProfilerChild.
+ MOZ_RELEASE_ASSERT(!lockedmThread ||
+ lockedmThread == NS_GetCurrentThread());
+ }
+ MOZ_ASSERT(aEndpoint.IsValid());
+
+ mProfilerChild = new ProfilerChild();
+ Endpoint<PProfilerChild> endpoint = std::move(aEndpoint);
+
+ if (!endpoint.Bind(mProfilerChild)) {
+ MOZ_CRASH("Failed to bind ProfilerChild!");
+ }
+}
+
+void ChildProfilerController::ShutdownProfilerChild(
+ ProfileAndAdditionalInformation* aOutShutdownProfileInformation) {
+ const bool isProfiling = profiler_is_active();
+ if (aOutShutdownProfileInformation) {
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::ProfilerChildShutdownPhase,
+ isProfiling ? "Profiling - GrabShutdownProfile"_ns
+ : "Not profiling - GrabShutdownProfile"_ns);
+ *aOutShutdownProfileInformation = mProfilerChild->GrabShutdownProfile();
+ }
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::ProfilerChildShutdownPhase,
+ isProfiling ? "Profiling - Destroying ProfilerChild"_ns
+ : "Not profiling - Destroying ProfilerChild"_ns);
+ mProfilerChild->Destroy();
+ mProfilerChild = nullptr;
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::ProfilerChildShutdownPhase,
+ isProfiling
+ ? "Profiling - ShutdownProfilerChild complete, waiting for thread shutdown"_ns
+ : "Not Profiling - ShutdownProfilerChild complete, waiting for thread shutdown"_ns);
+}
+
+} // namespace mozilla
diff --git a/tools/profiler/gecko/PProfiler.ipdl b/tools/profiler/gecko/PProfiler.ipdl
new file mode 100644
index 0000000000..65778b892c
--- /dev/null
+++ b/tools/profiler/gecko/PProfiler.ipdl
@@ -0,0 +1,44 @@
+/* -*- 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 ProfilerTypes;
+
+namespace mozilla {
+
+// PProfiler is a top-level protocol. It is used to let the main process
+// control the Gecko Profiler in other processes, and request profiles from
+// those processes.
+// It is a top-level protocol so that its child endpoint can be on a
+// background thread, so that profiles can be gathered even if the main thread
+// is unresponsive.
+[ChildImpl=virtual, ParentImpl=virtual]
+async protocol PProfiler
+{
+child:
+ // The unused returned value is to have a promise we can await.
+ async Start(ProfilerInitParams params) returns (bool unused);
+ async EnsureStarted(ProfilerInitParams params) returns (bool unused);
+ async Stop() returns (bool unused);
+ async Pause() returns (bool unused);
+ async Resume() returns (bool unused);
+ async PauseSampling() returns (bool unused);
+ async ResumeSampling() returns (bool unused);
+
+ async WaitOnePeriodicSampling() returns (bool sampled);
+
+ async AwaitNextChunkManagerUpdate() returns (ProfileBufferChunkManagerUpdate update);
+ async DestroyReleasedChunksAtOrBefore(TimeStamp timeStamp);
+
+ // The returned shmem may contain an empty string (unavailable), an error
+ // message starting with '*', or a profile as a stringified JSON object.
+ async GatherProfile() returns (IPCProfileAndAdditionalInformation profileAndAdditionalInformation);
+ async GetGatherProfileProgress() returns (GatherProfileProgress progress);
+
+ async ClearAllPages();
+};
+
+} // namespace mozilla
+
diff --git a/tools/profiler/gecko/ProfilerChild.cpp b/tools/profiler/gecko/ProfilerChild.cpp
new file mode 100644
index 0000000000..db7ef99423
--- /dev/null
+++ b/tools/profiler/gecko/ProfilerChild.cpp
@@ -0,0 +1,565 @@
+/* -*- 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 "ProfilerChild.h"
+
+#include "GeckoProfiler.h"
+#include "platform.h"
+#include "ProfilerCodeAddressService.h"
+#include "ProfilerControl.h"
+#include "ProfilerParent.h"
+
+#include "chrome/common/ipc_channel.h"
+#include "nsPrintfCString.h"
+#include "nsThreadUtils.h"
+
+#include <memory>
+
+namespace mozilla {
+
+/* static */ DataMutexBase<ProfilerChild::ProfilerChildAndUpdate,
+ baseprofiler::detail::BaseProfilerMutex>
+ ProfilerChild::sPendingChunkManagerUpdate{
+ "ProfilerChild::sPendingChunkManagerUpdate"};
+
+ProfilerChild::ProfilerChild()
+ : mThread(NS_GetCurrentThread()), mDestroyed(false) {
+ MOZ_COUNT_CTOR(ProfilerChild);
+}
+
+ProfilerChild::~ProfilerChild() { MOZ_COUNT_DTOR(ProfilerChild); }
+
+void ProfilerChild::ResolveChunkUpdate(
+ PProfilerChild::AwaitNextChunkManagerUpdateResolver& aResolve) {
+ MOZ_ASSERT(!!aResolve,
+ "ResolveChunkUpdate should only be called when there's a pending "
+ "resolver");
+ MOZ_ASSERT(
+ !mChunkManagerUpdate.IsNotUpdate(),
+ "ResolveChunkUpdate should only be called with a real or final update");
+ MOZ_ASSERT(
+ !mDestroyed,
+ "ResolveChunkUpdate should not be called if the actor was destroyed");
+ if (mChunkManagerUpdate.IsFinal()) {
+ // Final update, send a special "unreleased value", but don't clear the
+ // local copy so we know we got the final update.
+ std::move(aResolve)(ProfilerParent::MakeFinalUpdate());
+ } else {
+ // Optimization note: The ProfileBufferChunkManagerUpdate constructor takes
+ // the newly-released chunks nsTArray by reference-to-const, therefore
+ // constructing and then moving the array here would make a copy. So instead
+ // we first give it an empty array, and then we can write the data directly
+ // into the update's array.
+ ProfileBufferChunkManagerUpdate update{
+ mChunkManagerUpdate.UnreleasedBytes(),
+ mChunkManagerUpdate.ReleasedBytes(),
+ mChunkManagerUpdate.OldestDoneTimeStamp(),
+ {}};
+ update.newlyReleasedChunks().SetCapacity(
+ mChunkManagerUpdate.NewlyReleasedChunksRef().size());
+ for (const ProfileBufferControlledChunkManager::ChunkMetadata& chunk :
+ mChunkManagerUpdate.NewlyReleasedChunksRef()) {
+ update.newlyReleasedChunks().EmplaceBack(chunk.mDoneTimeStamp,
+ chunk.mBufferBytes);
+ }
+
+ std::move(aResolve)(update);
+
+ // Clear the update we just sent, so it's ready for later updates to be
+ // folded into it.
+ mChunkManagerUpdate.Clear();
+ }
+
+ // Discard the resolver, so it's empty next time there's a new request.
+ aResolve = nullptr;
+}
+
+void ProfilerChild::ProcessChunkManagerUpdate(
+ ProfileBufferControlledChunkManager::Update&& aUpdate) {
+ if (mDestroyed) {
+ return;
+ }
+ // Always store the data, it could be the final update.
+ mChunkManagerUpdate.Fold(std::move(aUpdate));
+ if (mAwaitNextChunkManagerUpdateResolver) {
+ // There is already a pending resolver, give it the info now.
+ ResolveChunkUpdate(mAwaitNextChunkManagerUpdateResolver);
+ }
+}
+
+/* static */ void ProfilerChild::ProcessPendingUpdate() {
+ auto lockedUpdate = sPendingChunkManagerUpdate.Lock();
+ if (!lockedUpdate->mProfilerChild || lockedUpdate->mUpdate.IsNotUpdate()) {
+ return;
+ }
+ lockedUpdate->mProfilerChild->mThread->Dispatch(NS_NewRunnableFunction(
+ "ProfilerChild::ProcessPendingUpdate", []() mutable {
+ auto lockedUpdate = sPendingChunkManagerUpdate.Lock();
+ if (!lockedUpdate->mProfilerChild ||
+ lockedUpdate->mUpdate.IsNotUpdate()) {
+ return;
+ }
+ lockedUpdate->mProfilerChild->ProcessChunkManagerUpdate(
+ std::move(lockedUpdate->mUpdate));
+ lockedUpdate->mUpdate.Clear();
+ }));
+}
+
+/* static */ bool ProfilerChild::IsLockedOnCurrentThread() {
+ return sPendingChunkManagerUpdate.Mutex().IsLockedOnCurrentThread();
+}
+
+void ProfilerChild::SetupChunkManager() {
+ mChunkManager = profiler_get_controlled_chunk_manager();
+ if (NS_WARN_IF(!mChunkManager)) {
+ return;
+ }
+
+ // Make sure there are no updates (from a previous run).
+ mChunkManagerUpdate.Clear();
+ {
+ auto lockedUpdate = sPendingChunkManagerUpdate.Lock();
+ lockedUpdate->mProfilerChild = this;
+ lockedUpdate->mUpdate.Clear();
+ }
+
+ mChunkManager->SetUpdateCallback(
+ [](ProfileBufferControlledChunkManager::Update&& aUpdate) {
+ // Updates from the chunk manager are stored for later processing.
+ // We avoid dispatching a task, as this could deadlock (if the queueing
+ // mutex is held elsewhere).
+ auto lockedUpdate = sPendingChunkManagerUpdate.Lock();
+ if (!lockedUpdate->mProfilerChild) {
+ return;
+ }
+ lockedUpdate->mUpdate.Fold(std::move(aUpdate));
+ });
+}
+
+void ProfilerChild::ResetChunkManager() {
+ if (!mChunkManager) {
+ return;
+ }
+
+ // We have a chunk manager, reset the callback, which will add a final
+ // pending update.
+ mChunkManager->SetUpdateCallback({});
+
+ // Clear the pending update.
+ auto lockedUpdate = sPendingChunkManagerUpdate.Lock();
+ lockedUpdate->mProfilerChild = nullptr;
+ lockedUpdate->mUpdate.Clear();
+ // And process a final update right now.
+ ProcessChunkManagerUpdate(
+ ProfileBufferControlledChunkManager::Update(nullptr));
+
+ mChunkManager = nullptr;
+ mAwaitNextChunkManagerUpdateResolver = nullptr;
+}
+
+mozilla::ipc::IPCResult ProfilerChild::RecvStart(
+ const ProfilerInitParams& params, StartResolver&& aResolve) {
+ nsTArray<const char*> filterArray;
+ for (size_t i = 0; i < params.filters().Length(); ++i) {
+ filterArray.AppendElement(params.filters()[i].get());
+ }
+
+ profiler_start(PowerOfTwo32(params.entries()), params.interval(),
+ params.features(), filterArray.Elements(),
+ filterArray.Length(), params.activeTabID(), params.duration());
+
+ SetupChunkManager();
+
+ aResolve(/* unused */ true);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ProfilerChild::RecvEnsureStarted(
+ const ProfilerInitParams& params, EnsureStartedResolver&& aResolve) {
+ nsTArray<const char*> filterArray;
+ for (size_t i = 0; i < params.filters().Length(); ++i) {
+ filterArray.AppendElement(params.filters()[i].get());
+ }
+
+ profiler_ensure_started(PowerOfTwo32(params.entries()), params.interval(),
+ params.features(), filterArray.Elements(),
+ filterArray.Length(), params.activeTabID(),
+ params.duration());
+
+ SetupChunkManager();
+
+ aResolve(/* unused */ true);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ProfilerChild::RecvStop(StopResolver&& aResolve) {
+ ResetChunkManager();
+ profiler_stop();
+ aResolve(/* unused */ true);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ProfilerChild::RecvPause(PauseResolver&& aResolve) {
+ profiler_pause();
+ aResolve(/* unused */ true);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ProfilerChild::RecvResume(ResumeResolver&& aResolve) {
+ profiler_resume();
+ aResolve(/* unused */ true);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ProfilerChild::RecvPauseSampling(
+ PauseSamplingResolver&& aResolve) {
+ profiler_pause_sampling();
+ aResolve(/* unused */ true);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ProfilerChild::RecvResumeSampling(
+ ResumeSamplingResolver&& aResolve) {
+ profiler_resume_sampling();
+ aResolve(/* unused */ true);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ProfilerChild::RecvWaitOnePeriodicSampling(
+ WaitOnePeriodicSamplingResolver&& aResolve) {
+ std::shared_ptr<WaitOnePeriodicSamplingResolver> resolve =
+ std::make_shared<WaitOnePeriodicSamplingResolver>(std::move(aResolve));
+ if (!profiler_callback_after_sampling(
+ [self = RefPtr(this), resolve](SamplingState aSamplingState) mutable {
+ if (self->mDestroyed) {
+ return;
+ }
+ MOZ_RELEASE_ASSERT(self->mThread);
+ self->mThread->Dispatch(NS_NewRunnableFunction(
+ "nsProfiler::WaitOnePeriodicSampling result on main thread",
+ [resolve = std::move(resolve), aSamplingState]() {
+ (*resolve)(aSamplingState ==
+ SamplingState::SamplingCompleted ||
+ aSamplingState ==
+ SamplingState::NoStackSamplingCompleted);
+ }));
+ })) {
+ // Callback was not added (e.g., profiler is not running) and will never be
+ // invoked, so we need to resolve the promise here.
+ (*resolve)(false);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ProfilerChild::RecvClearAllPages() {
+ profiler_clear_all_pages();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ProfilerChild::RecvAwaitNextChunkManagerUpdate(
+ AwaitNextChunkManagerUpdateResolver&& aResolve) {
+ MOZ_ASSERT(!mDestroyed,
+ "Recv... should not be called if the actor was destroyed");
+ // Pick up pending updates if any.
+ {
+ auto lockedUpdate = sPendingChunkManagerUpdate.Lock();
+ if (lockedUpdate->mProfilerChild && !lockedUpdate->mUpdate.IsNotUpdate()) {
+ mChunkManagerUpdate.Fold(std::move(lockedUpdate->mUpdate));
+ lockedUpdate->mUpdate.Clear();
+ }
+ }
+ if (mChunkManagerUpdate.IsNotUpdate()) {
+ // No data yet, store the resolver for later.
+ mAwaitNextChunkManagerUpdateResolver = std::move(aResolve);
+ } else {
+ // We have data, send it now.
+ ResolveChunkUpdate(aResolve);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ProfilerChild::RecvDestroyReleasedChunksAtOrBefore(
+ const TimeStamp& aTimeStamp) {
+ if (mChunkManager) {
+ mChunkManager->DestroyChunksAtOrBefore(aTimeStamp);
+ }
+ return IPC_OK();
+}
+
+struct GatherProfileThreadParameters
+ : public external::AtomicRefCounted<GatherProfileThreadParameters> {
+ MOZ_DECLARE_REFCOUNTED_TYPENAME(GatherProfileThreadParameters)
+
+ GatherProfileThreadParameters(
+ RefPtr<ProfilerChild> aProfilerChild,
+ RefPtr<ProgressLogger::SharedProgress> aProgress,
+ ProfilerChild::GatherProfileResolver&& aResolver)
+ : profilerChild(std::move(aProfilerChild)),
+ progress(std::move(aProgress)),
+ resolver(std::move(aResolver)) {}
+
+ RefPtr<ProfilerChild> profilerChild;
+
+ FailureLatchSource failureLatchSource;
+
+ // Separate RefPtr used when working on separate thread. This way, if the
+ // "ProfilerChild" thread decides to overwrite its mGatherProfileProgress with
+ // a new one, the work done here will still only use the old one.
+ RefPtr<ProgressLogger::SharedProgress> progress;
+
+ // Resolver for the GatherProfile promise. Must only be called on the
+ // "ProfilerChild" thread.
+ ProfilerChild::GatherProfileResolver resolver;
+};
+
+/* static */
+void ProfilerChild::GatherProfileThreadFunction(
+ void* already_AddRefedParameters) {
+ PR_SetCurrentThreadName("GatherProfileThread");
+
+ RefPtr<GatherProfileThreadParameters> parameters =
+ already_AddRefed<GatherProfileThreadParameters>{
+ static_cast<GatherProfileThreadParameters*>(
+ already_AddRefedParameters)};
+
+ ProgressLogger progressLogger(
+ parameters->progress, "Gather-profile thread started", "Profile sent");
+ using namespace mozilla::literals::ProportionValue_literals; // For `1_pc`.
+
+ auto writer =
+ MakeUnique<SpliceableChunkedJSONWriter>(parameters->failureLatchSource);
+ if (!profiler_get_profile_json(
+ *writer,
+ /* aSinceTime */ 0,
+ /* aIsShuttingDown */ false,
+ progressLogger.CreateSubLoggerFromTo(
+ 1_pc, "profiler_get_profile_json started", 99_pc,
+ "profiler_get_profile_json done"))) {
+ // Failed to get a profile, reset the writer pointer, so that we'll send a
+ // failure message.
+ writer.reset();
+ }
+
+ if (NS_WARN_IF(NS_FAILED(
+ parameters->profilerChild->mThread->Dispatch(NS_NewRunnableFunction(
+ "ProfilerChild::ProcessPendingUpdate",
+ [parameters,
+ // Forward progress logger to on-ProfilerChild-thread task, so
+ // that it doesn't get marked as 100% done when this off-thread
+ // function ends.
+ progressLogger = std::move(progressLogger),
+ writer = std::move(writer)]() mutable {
+ // We are now on the ProfilerChild thread, about to send the
+ // completed profile. Any incoming progress request will now be
+ // handled after this task ends, so updating the progress is now
+ // useless and we can just get rid of the progress storage.
+ if (parameters->profilerChild->mGatherProfileProgress ==
+ parameters->progress) {
+ // The ProfilerChild progress is still the one we know.
+ parameters->profilerChild->mGatherProfileProgress = nullptr;
+ }
+
+ // Shmem allocation and promise resolution must be made on the
+ // ProfilerChild thread, that's why this task was needed here.
+ mozilla::ipc::Shmem shmem;
+ if (writer) {
+ if (const size_t len = writer->ChunkedWriteFunc().Length();
+ len < UINT32_MAX) {
+ bool shmemSuccess = true;
+ const bool copySuccess =
+ writer->ChunkedWriteFunc()
+ .CopyDataIntoLazilyAllocatedBuffer(
+ [&](size_t allocationSize) -> char* {
+ MOZ_ASSERT(allocationSize == len + 1);
+ if (parameters->profilerChild->AllocShmem(
+ allocationSize, &shmem)) {
+ return shmem.get<char>();
+ }
+ shmemSuccess = false;
+ return nullptr;
+ });
+ if (!shmemSuccess || !copySuccess) {
+ const nsPrintfCString message(
+ (!shmemSuccess)
+ ? "*Could not create shmem for profile from pid "
+ "%u (%zu B)"
+ : "*Could not write profile from pid %u (%zu B)",
+ unsigned(profiler_current_process_id().ToNumber()),
+ len);
+ if (parameters->profilerChild->AllocShmem(
+ message.Length() + 1, &shmem)) {
+ strcpy(shmem.get<char>(), message.Data());
+ }
+ }
+ } else {
+ const nsPrintfCString message(
+ "*Profile from pid %u bigger (%zu) than shmem max "
+ "(%zu)",
+ unsigned(profiler_current_process_id().ToNumber()), len,
+ size_t(UINT32_MAX));
+ if (parameters->profilerChild->AllocShmem(
+ message.Length() + 1, &shmem)) {
+ strcpy(shmem.get<char>(), message.Data());
+ }
+ }
+ writer = nullptr;
+ } else {
+ // No profile.
+ const char* failure =
+ parameters->failureLatchSource.GetFailure();
+ const nsPrintfCString message(
+ "*Could not generate profile from pid %u%s%s",
+ unsigned(profiler_current_process_id().ToNumber()),
+ failure ? ", failure: " : "", failure ? failure : "");
+ if (parameters->profilerChild->AllocShmem(
+ message.Length() + 1, &shmem)) {
+ strcpy(shmem.get<char>(), message.Data());
+ }
+ }
+
+ SharedLibraryInfo sharedLibraryInfo =
+ SharedLibraryInfo::GetInfoForSelf();
+ parameters->resolver(IPCProfileAndAdditionalInformation{
+ shmem, Some(ProfileGenerationAdditionalInformation{
+ std::move(sharedLibraryInfo)})});
+ }))))) {
+ // Failed to dispatch the task to the ProfilerChild thread. The IPC cannot
+ // be resolved on this thread, so it will never be resolved!
+ // And it would be unsafe to modify mGatherProfileProgress; But the parent
+ // should notice that's it's not advancing anymore.
+ }
+}
+
+mozilla::ipc::IPCResult ProfilerChild::RecvGatherProfile(
+ GatherProfileResolver&& aResolve) {
+ mGatherProfileProgress = MakeRefPtr<ProgressLogger::SharedProgress>();
+ mGatherProfileProgress->SetProgress(ProportionValue{0.0},
+ "Received gather-profile request");
+
+ auto parameters = MakeRefPtr<GatherProfileThreadParameters>(
+ this, mGatherProfileProgress, std::move(aResolve));
+
+ // The GatherProfileThreadFunction thread function will cast its void*
+ // argument to already_AddRefed<GatherProfileThreadParameters>.
+ parameters.get()->AddRef();
+ PRThread* gatherProfileThread = PR_CreateThread(
+ PR_SYSTEM_THREAD, GatherProfileThreadFunction, parameters.get(),
+ PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD, PR_UNJOINABLE_THREAD, 0);
+
+ if (!gatherProfileThread) {
+ // Failed to create and start worker thread, resolve with an empty profile.
+ mozilla::ipc::Shmem shmem;
+ if (AllocShmem(1, &shmem)) {
+ shmem.get<char>()[0] = '\0';
+ }
+ parameters->resolver(IPCProfileAndAdditionalInformation{shmem, Nothing()});
+ // And clean up.
+ parameters.get()->Release();
+ mGatherProfileProgress = nullptr;
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ProfilerChild::RecvGetGatherProfileProgress(
+ GetGatherProfileProgressResolver&& aResolve) {
+ if (mGatherProfileProgress) {
+ aResolve(GatherProfileProgress{
+ mGatherProfileProgress->Progress().ToUnderlyingType(),
+ nsCString(mGatherProfileProgress->LastLocation())});
+ } else {
+ aResolve(
+ GatherProfileProgress{ProportionValue::MakeInvalid().ToUnderlyingType(),
+ nsCString("No gather-profile in progress")});
+ }
+ return IPC_OK();
+}
+
+void ProfilerChild::ActorDestroy(ActorDestroyReason aActorDestroyReason) {
+ mDestroyed = true;
+}
+
+void ProfilerChild::Destroy() {
+ ResetChunkManager();
+ if (!mDestroyed) {
+ Close();
+ }
+}
+
+ProfileAndAdditionalInformation ProfilerChild::GrabShutdownProfile() {
+ LOG("GrabShutdownProfile");
+
+ UniquePtr<ProfilerCodeAddressService> service =
+ profiler_code_address_service_for_presymbolication();
+ FailureLatchSource failureLatch;
+ SpliceableChunkedJSONWriter writer{failureLatch};
+ writer.Start();
+ auto rv = profiler_stream_json_for_this_process(
+ writer, /* aSinceTime */ 0,
+ /* aIsShuttingDown */ true, service.get(), ProgressLogger{});
+ if (rv.isErr()) {
+ const char* failure = writer.GetFailure();
+ return ProfileAndAdditionalInformation(
+ nsPrintfCString("*Profile unavailable for pid %u%s%s",
+ unsigned(profiler_current_process_id().ToNumber()),
+ failure ? ", failure: " : "", failure ? failure : ""));
+ }
+
+ auto additionalInfo = rv.unwrap();
+
+ writer.StartArrayProperty("processes");
+ writer.EndArray();
+ writer.End();
+
+ const size_t len = writer.ChunkedWriteFunc().Length();
+ // This string and information are destined to be sent as a shutdown profile,
+ // which is limited by the maximum IPC message size.
+ // TODO: IPC to change to shmem (bug 1780330), raising this limit to
+ // JS::MaxStringLength.
+ if (len + additionalInfo.SizeOf() >=
+ size_t(IPC::Channel::kMaximumMessageSize)) {
+ return ProfileAndAdditionalInformation(
+ nsPrintfCString("*Profile from pid %u bigger (%zu) than IPC max (%zu)",
+ unsigned(profiler_current_process_id().ToNumber()), len,
+ size_t(IPC::Channel::kMaximumMessageSize)));
+ }
+
+ nsCString profileCString;
+ if (!profileCString.SetLength(len, fallible)) {
+ return ProfileAndAdditionalInformation(nsPrintfCString(
+ "*Could not allocate %zu bytes for profile from pid %u", len,
+ unsigned(profiler_current_process_id().ToNumber())));
+ }
+ MOZ_ASSERT(*(profileCString.Data() + len) == '\0',
+ "We expected a null at the end of the string buffer, to be "
+ "rewritten by CopyDataIntoLazilyAllocatedBuffer");
+
+ char* const profileBeginWriting = profileCString.BeginWriting();
+ if (!profileBeginWriting) {
+ return ProfileAndAdditionalInformation(
+ nsPrintfCString("*Could not write profile from pid %u",
+ unsigned(profiler_current_process_id().ToNumber())));
+ }
+
+ // Here, we have enough space reserved in `profileCString`, starting at
+ // `profileBeginWriting`, copy the JSON profile there.
+ if (!writer.ChunkedWriteFunc().CopyDataIntoLazilyAllocatedBuffer(
+ [&](size_t aBufferLen) -> char* {
+ MOZ_RELEASE_ASSERT(aBufferLen == len + 1);
+ return profileBeginWriting;
+ })) {
+ return ProfileAndAdditionalInformation(
+ nsPrintfCString("*Could not copy profile from pid %u",
+ unsigned(profiler_current_process_id().ToNumber())));
+ }
+ MOZ_ASSERT(*(profileCString.Data() + len) == '\0',
+ "We still expected a null at the end of the string buffer");
+
+ return ProfileAndAdditionalInformation{std::move(profileCString),
+ std::move(additionalInfo)};
+}
+
+} // namespace mozilla
diff --git a/tools/profiler/gecko/ProfilerIOInterposeObserver.cpp b/tools/profiler/gecko/ProfilerIOInterposeObserver.cpp
new file mode 100644
index 0000000000..cf33789f69
--- /dev/null
+++ b/tools/profiler/gecko/ProfilerIOInterposeObserver.cpp
@@ -0,0 +1,216 @@
+/* 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 "ProfilerIOInterposeObserver.h"
+#include "GeckoProfiler.h"
+
+using namespace mozilla;
+
+/* static */
+ProfilerIOInterposeObserver& ProfilerIOInterposeObserver::GetInstance() {
+ static ProfilerIOInterposeObserver sProfilerIOInterposeObserver;
+ return sProfilerIOInterposeObserver;
+}
+
+namespace geckoprofiler::markers {
+struct FileIOMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("FileIO");
+ }
+ static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter,
+ const ProfilerString8View& aOperation,
+ const ProfilerString8View& aSource,
+ const ProfilerString8View& aFilename,
+ MarkerThreadId aOperationThreadId) {
+ aWriter.StringProperty("operation", aOperation);
+ aWriter.StringProperty("source", aSource);
+ if (aFilename.Length() != 0) {
+ aWriter.StringProperty("filename", aFilename);
+ }
+ if (!aOperationThreadId.IsUnspecified()) {
+ // Tech note: If `ToNumber()` returns a uint64_t, the conversion to
+ // int64_t is "implementation-defined" before C++20. This is acceptable
+ // here, because this is a one-way conversion to a unique identifier
+ // that's used to visually separate data by thread on the front-end.
+ aWriter.IntProperty(
+ "threadId",
+ static_cast<int64_t>(aOperationThreadId.ThreadId().ToNumber()));
+ }
+ }
+ static MarkerSchema MarkerTypeDisplay() {
+ using MS = MarkerSchema;
+ MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable,
+ MS::Location::TimelineFileIO};
+ schema.AddKeyLabelFormatSearchable("operation", "Operation",
+ MS::Format::String,
+ MS::Searchable::Searchable);
+ schema.AddKeyLabelFormatSearchable("source", "Source", MS::Format::String,
+ MS::Searchable::Searchable);
+ schema.AddKeyLabelFormatSearchable("filename", "Filename",
+ MS::Format::FilePath,
+ MS::Searchable::Searchable);
+ schema.AddKeyLabelFormatSearchable("threadId", "Thread ID",
+ MS::Format::String,
+ MS::Searchable::Searchable);
+ return schema;
+ }
+};
+} // namespace geckoprofiler::markers
+
+static auto GetFilename(IOInterposeObserver::Observation& aObservation) {
+ AUTO_PROFILER_STATS(IO_filename);
+ constexpr size_t scExpectedMaxFilename = 512;
+ nsAutoStringN<scExpectedMaxFilename> filename16;
+ aObservation.Filename(filename16);
+ nsAutoCStringN<scExpectedMaxFilename> filename8;
+ if (!filename16.IsEmpty()) {
+ CopyUTF16toUTF8(filename16, filename8);
+ }
+ return filename8;
+}
+
+void ProfilerIOInterposeObserver::Observe(Observation& aObservation) {
+ if (profiler_is_locked_on_current_thread()) {
+ // Don't observe I/Os originating from the profiler itself (when internally
+ // locked) to avoid deadlocks when calling profiler functions.
+ AUTO_PROFILER_STATS(IO_profiler_locked);
+ return;
+ }
+
+ Maybe<uint32_t> maybeFeatures = profiler_features_if_active_and_unpaused();
+ if (maybeFeatures.isNothing()) {
+ return;
+ }
+ uint32_t features = *maybeFeatures;
+
+ if (!profiler_thread_is_being_profiled_for_markers(
+ profiler_main_thread_id()) &&
+ !profiler_thread_is_being_profiled_for_markers()) {
+ return;
+ }
+
+ AUTO_PROFILER_LABEL("ProfilerIOInterposeObserver", PROFILER);
+ if (IsMainThread()) {
+ // This is the main thread.
+ // Capture a marker if any "IO" feature is on.
+ // If it's not being profiled, we have nowhere to store FileIO markers.
+ if (!profiler_thread_is_being_profiled_for_markers() ||
+ !(features & ProfilerFeature::MainThreadIO)) {
+ return;
+ }
+ AUTO_PROFILER_STATS(IO_MT);
+ nsAutoCString type{aObservation.FileType()};
+ type.AppendLiteral("IO");
+
+ // Store the marker in the current thread.
+ PROFILER_MARKER(
+ type, OTHER,
+ MarkerOptions(
+ MarkerTiming::Interval(aObservation.Start(), aObservation.End()),
+ MarkerStack::Capture()),
+ FileIOMarker,
+ // aOperation
+ ProfilerString8View::WrapNullTerminatedString(
+ aObservation.ObservedOperationString()),
+ // aSource
+ ProfilerString8View::WrapNullTerminatedString(aObservation.Reference()),
+ // aFilename
+ GetFilename(aObservation),
+ // aOperationThreadId - Do not include a thread ID, as it's the same as
+ // the markers. Only include this field when the marker is being sent
+ // from another thread.
+ MarkerThreadId{});
+
+ } else if (profiler_thread_is_being_profiled_for_markers()) {
+ // This is a non-main thread that is being profiled.
+ if (!(features & ProfilerFeature::FileIO)) {
+ return;
+ }
+ AUTO_PROFILER_STATS(IO_off_MT);
+
+ nsAutoCString type{aObservation.FileType()};
+ type.AppendLiteral("IO");
+
+ // Share a backtrace between the marker on this thread, and the marker on
+ // the main thread.
+ UniquePtr<ProfileChunkedBuffer> backtrace = profiler_capture_backtrace();
+
+ // Store the marker in the current thread.
+ PROFILER_MARKER(
+ type, OTHER,
+ MarkerOptions(
+ MarkerTiming::Interval(aObservation.Start(), aObservation.End()),
+ backtrace ? MarkerStack::UseBacktrace(*backtrace)
+ : MarkerStack::NoStack()),
+ FileIOMarker,
+ // aOperation
+ ProfilerString8View::WrapNullTerminatedString(
+ aObservation.ObservedOperationString()),
+ // aSource
+ ProfilerString8View::WrapNullTerminatedString(aObservation.Reference()),
+ // aFilename
+ GetFilename(aObservation),
+ // aOperationThreadId - Do not include a thread ID, as it's the same as
+ // the markers. Only include this field when the marker is being sent
+ // from another thread.
+ MarkerThreadId{});
+
+ // Store the marker in the main thread as well, with a distinct marker name
+ // and thread id.
+ type.AppendLiteral(" (non-main thread)");
+ PROFILER_MARKER(
+ type, OTHER,
+ MarkerOptions(
+ MarkerTiming::Interval(aObservation.Start(), aObservation.End()),
+ backtrace ? MarkerStack::UseBacktrace(*backtrace)
+ : MarkerStack::NoStack(),
+ // This is the important piece that changed.
+ // It will send a marker to the main thread.
+ MarkerThreadId::MainThread()),
+ FileIOMarker,
+ // aOperation
+ ProfilerString8View::WrapNullTerminatedString(
+ aObservation.ObservedOperationString()),
+ // aSource
+ ProfilerString8View::WrapNullTerminatedString(aObservation.Reference()),
+ // aFilename
+ GetFilename(aObservation),
+ // aOperationThreadId - Include the thread ID in the payload.
+ MarkerThreadId::CurrentThread());
+
+ } else {
+ // This is a thread that is not being profiled. We still want to capture
+ // file I/Os (to the main thread) if the "FileIOAll" feature is on.
+ if (!(features & ProfilerFeature::FileIOAll)) {
+ return;
+ }
+ AUTO_PROFILER_STATS(IO_other);
+ nsAutoCString type{aObservation.FileType()};
+ if (profiler_is_active_and_thread_is_registered()) {
+ type.AppendLiteral("IO (non-profiled thread)");
+ } else {
+ type.AppendLiteral("IO (unregistered thread)");
+ }
+
+ // Only store this marker on the main thread, as this thread was not being
+ // profiled.
+ PROFILER_MARKER(
+ type, OTHER,
+ MarkerOptions(
+ MarkerTiming::Interval(aObservation.Start(), aObservation.End()),
+ MarkerStack::Capture(),
+ // Store this marker on the main thread.
+ MarkerThreadId::MainThread()),
+ FileIOMarker,
+ // aOperation
+ ProfilerString8View::WrapNullTerminatedString(
+ aObservation.ObservedOperationString()),
+ // aSource
+ ProfilerString8View::WrapNullTerminatedString(aObservation.Reference()),
+ // aFilename
+ GetFilename(aObservation),
+ // aOperationThreadId - Note which thread this marker is coming from.
+ MarkerThreadId::CurrentThread());
+ }
+}
diff --git a/tools/profiler/gecko/ProfilerIOInterposeObserver.h b/tools/profiler/gecko/ProfilerIOInterposeObserver.h
new file mode 100644
index 0000000000..9e22a34f15
--- /dev/null
+++ b/tools/profiler/gecko/ProfilerIOInterposeObserver.h
@@ -0,0 +1,32 @@
+/* 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/. */
+
+#ifndef PROFILERIOINTERPOSEOBSERVER_H
+#define PROFILERIOINTERPOSEOBSERVER_H
+
+#include "mozilla/IOInterposer.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+/**
+ * This class is the observer that calls into the profiler whenever
+ * main thread I/O occurs.
+ */
+class ProfilerIOInterposeObserver final : public IOInterposeObserver {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ProfilerIOInterposeObserver)
+
+ public:
+ static ProfilerIOInterposeObserver& GetInstance();
+
+ virtual void Observe(Observation& aObservation) override;
+
+ private:
+ ProfilerIOInterposeObserver() = default;
+ virtual ~ProfilerIOInterposeObserver() {}
+};
+
+} // namespace mozilla
+
+#endif // PROFILERIOINTERPOSEOBSERVER_H
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
diff --git a/tools/profiler/gecko/ProfilerTypes.ipdlh b/tools/profiler/gecko/ProfilerTypes.ipdlh
new file mode 100644
index 0000000000..6255d47db0
--- /dev/null
+++ b/tools/profiler/gecko/ProfilerTypes.ipdlh
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
+/* 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/. */
+
+using class mozilla::TimeStamp from "mozilla/TimeStamp.h";
+using struct mozilla::ProfileGenerationAdditionalInformation from "ProfileAdditionalInformation.h";
+
+namespace mozilla {
+
+struct ProfilerInitParams {
+ bool enabled;
+ uint32_t entries;
+ double? duration;
+ double interval;
+ uint32_t features;
+ uint64_t activeTabID;
+ nsCString[] filters;
+};
+
+struct ProfileBufferChunkMetadata {
+ TimeStamp doneTimeStamp;
+ uint32_t bufferBytes;
+};
+
+struct ProfileBufferChunkManagerUpdate {
+ uint64_t unreleasedBytes;
+ uint64_t releasedBytes;
+ TimeStamp oldestDoneTimeStamp;
+ ProfileBufferChunkMetadata[] newlyReleasedChunks;
+};
+
+struct GatherProfileProgress {
+ uint32_t progressProportionValueUnderlyingType;
+ nsCString progressLocation;
+};
+
+struct IPCProfileAndAdditionalInformation {
+ Shmem profileShmem;
+ ProfileGenerationAdditionalInformation? additionalInformation;
+};
+
+} // namespace mozilla
diff --git a/tools/profiler/gecko/components.conf b/tools/profiler/gecko/components.conf
new file mode 100644
index 0000000000..b1775c37ab
--- /dev/null
+++ b/tools/profiler/gecko/components.conf
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+Classes = [
+ {
+ 'js_name': 'profiler',
+ 'cid': '{25db9b8e-8123-4de1-b66d-8bbbedf2cdf4}',
+ 'contract_ids': ['@mozilla.org/tools/profiler;1'],
+ 'interfaces': ['nsIProfiler'],
+ 'type': 'nsProfiler',
+ 'headers': ['/tools/profiler/gecko/nsProfiler.h'],
+ 'init_method': 'Init',
+ },
+]
diff --git a/tools/profiler/gecko/nsIProfiler.idl b/tools/profiler/gecko/nsIProfiler.idl
new file mode 100644
index 0000000000..8b501d4b9f
--- /dev/null
+++ b/tools/profiler/gecko/nsIProfiler.idl
@@ -0,0 +1,208 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "nsISupports.idl"
+
+%{C++
+#include "mozilla/Maybe.h"
+#include "nsTArrayForwardDeclare.h"
+#include "nsStringFwd.h"
+#include "mozilla/MozPromise.h"
+%}
+
+[ref] native nsCString(const nsCString);
+[ref] native StringArrayRef(const nsTArray<nsCString>);
+native ProfileDataBufferMozPromise(RefPtr<mozilla::MozPromise<FallibleTArray<uint8_t>, nsresult, true>>);
+
+/**
+ * Start-up parameters for subprocesses are passed through nsIObserverService,
+ * which, unfortunately, means we need to implement nsISupports in order to
+ * go through it.
+ */
+[scriptable, builtinclass, uuid(0a175ba7-8fcf-4ce9-9c4b-ccc6272f4425)]
+interface nsIProfilerStartParams : nsISupports
+{
+ readonly attribute uint32_t entries;
+ readonly attribute double duration;
+ readonly attribute double interval;
+ readonly attribute uint32_t features;
+ readonly attribute uint64_t activeTabID;
+
+ [noscript, notxpcom, nostdcall] StringArrayRef getFilters();
+};
+
+[scriptable, builtinclass, uuid(ead3f75c-0e0e-4fbb-901c-1e5392ef5b2a)]
+interface nsIProfiler : nsISupports
+{
+ /*
+ * Control functions return as soon as this process' profiler has done its
+ * work. The returned promise gets resolved when sub-processes have completed
+ * their operation, or immediately if there are no sub-processes.
+ */
+ [implicit_jscontext]
+ Promise StartProfiler(in uint32_t aEntries, in double aInterval,
+ in Array<AUTF8String> aFeatures,
+ [optional] in Array<AUTF8String> aFilters,
+ [optional] in uint64_t aActiveTabID,
+ [optional] in double aDuration);
+ [implicit_jscontext]
+ Promise StopProfiler();
+ boolean IsPaused();
+ [implicit_jscontext]
+ Promise Pause();
+ [implicit_jscontext]
+ Promise Resume();
+ boolean IsSamplingPaused();
+ [implicit_jscontext]
+ Promise PauseSampling();
+ [implicit_jscontext]
+ Promise ResumeSampling();
+
+ /*
+ * Resolves the returned promise after at least one full periodic sampling in
+ * each process.
+ * Rejects the promise if sampler is not running (yet, or anymore, or paused)
+ * in the parent process.
+ * This is mainly useful in tests, to wait just long enough to guarantee that
+ * at least one sample was taken in each process.
+ */
+ [implicit_jscontext]
+ Promise waitOnePeriodicSampling();
+
+ /*
+ * Returns the JSON string of the profile. If aSinceTime is passed, only
+ * report samples taken at >= aSinceTime.
+ */
+ string GetProfile([optional] in double aSinceTime);
+
+ /*
+ * Returns a JS object of the profile. If aSinceTime is passed, only report
+ * samples taken at >= aSinceTime.
+ */
+ [implicit_jscontext]
+ jsval getProfileData([optional] in double aSinceTime);
+
+ [implicit_jscontext]
+ Promise getProfileDataAsync([optional] in double aSinceTime);
+
+ [implicit_jscontext]
+ Promise getProfileDataAsArrayBuffer([optional] in double aSinceTime);
+
+ [implicit_jscontext]
+ Promise getProfileDataAsGzippedArrayBuffer([optional] in double aSinceTime);
+
+ /**
+ * Asynchronously dump the profile collected so far to a file.
+ * Returns a promise that resolves once the file has been written, with data
+ * from all responsive Firefox processes. Note: This blocks the parent process
+ * while collecting its own data, then unblocks while child processes data is
+ * being collected.
+ * `aFilename` may be a full path, or a path relative to where Firefox was
+ * launched. The target directory must already exist.
+ */
+ [implicit_jscontext]
+ Promise dumpProfileToFileAsync(in ACString aFilename,
+ [optional] in double aSinceTime);
+
+ /**
+ * Synchronously dump the profile collected so far in this process to a file.
+ * This profile will only contain data from the parent process, and from child
+ * processes that have ended during the session; other currently-live
+ * processes are ignored.
+ * `aFilename` may be a full path, or a path relative to where Firefox was
+ * launched. The target directory must already exist.
+ */
+ void dumpProfileToFile(in string aFilename);
+
+ boolean IsActive();
+
+ /**
+ * Clear all registered and unregistered page information in prifiler.
+ */
+ void ClearAllPages();
+
+ /**
+ * Returns an array of the features that are supported in this build.
+ * Features may vary depending on platform and build flags.
+ */
+ Array<AUTF8String> GetFeatures();
+
+ /**
+ * Returns a JavaScript object that contains a description of the currently configured
+ * state of the profiler when the profiler is active. This can be useful to assert
+ * the UI of the profiler's recording panel in tests. It returns null when the profiler
+ * is not active.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval activeConfiguration;
+
+ /**
+ * Returns an array of all features that are supported by the profiler.
+ * The array may contain features that are not supported in this build.
+ */
+ Array<AUTF8String> GetAllFeatures();
+
+ void GetBufferInfo(out uint32_t aCurrentPosition, out uint32_t aTotalSize,
+ out uint32_t aGeneration);
+
+ /**
+ * Returns the elapsed time, in milliseconds, since the profiler's epoch.
+ * The epoch is guaranteed to be constant for the duration of the
+ * process, but is otherwise arbitrary.
+ */
+ double getElapsedTime();
+
+ /**
+ * Contains an array of shared library objects.
+ * Every object has the properties:
+ * - start: The start address of the memory region occupied by this library.
+ * - end: The end address of the memory region occupied by this library.
+ * - offset: Usually zero, except on Linux / Android if the first mapped
+ * section of the library has been mapped to an address that's
+ * different from the library's base address.
+ * Then offset = start - baseAddress.
+ * - name: The name (file basename) of the binary.
+ * - path: The full absolute path to the binary.
+ * - debugName: On Windows, the name of the pdb file for the binary. On other
+ * platforms, the same as |name|.
+ * - debugPath: On Windows, the full absolute path of the pdb file for the
+ * binary. On other platforms, the same as |path|.
+ * - arch: On Mac, the name of the architecture that identifies the right
+ * binary image of a fat binary. Example values are "i386", "x86_64",
+ * and "x86_64h". (x86_64h is used for binaries that contain
+ * instructions that are specific to the Intel Haswell microarchitecture.)
+ * On non-Mac platforms, arch is "".
+ * - breakpadId: A unique identifier string for this library, as used by breakpad.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval sharedLibraries;
+
+ /**
+ * Returns a promise that resolves to a SymbolTableAsTuple for the binary at
+ * the given path.
+ *
+ * SymbolTable as tuple: [addrs, index, buffer]
+ * Contains a symbol table, which can be used to map addresses to strings.
+ *
+ * The first element of this tuple, commonly named "addrs", is a sorted array of
+ * symbol addresses, as library-relative offsets in bytes, in ascending order.
+ * The third element of this tuple, commonly named "buffer", is a buffer of
+ * bytes that contains all strings from this symbol table, in the order of the
+ * addresses they correspond to, in utf-8 encoded form, all concatenated
+ * together.
+ * The second element of this tuple, commonly named "index", contains positions
+ * into "buffer". For every address, that position is where the string for that
+ * address starts in the buffer.
+ * index.length == addrs.length + 1.
+ * index[addrs.length] is the end position of the last string in the buffer.
+ *
+ * The string for the address addrs[i] is
+ * (new TextDecoder()).decode(buffer.subarray(index[i], index[i + 1]))
+ */
+ [implicit_jscontext]
+ Promise getSymbolTable(in ACString aDebugPath, in ACString aBreakpadID);
+
+ [notxpcom, nostdcall] ProfileDataBufferMozPromise getProfileDataAsGzippedArrayBufferAndroid(in double aSinceTime);
+};
diff --git a/tools/profiler/gecko/nsProfiler.cpp b/tools/profiler/gecko/nsProfiler.cpp
new file mode 100644
index 0000000000..66e32ce2bc
--- /dev/null
+++ b/tools/profiler/gecko/nsProfiler.cpp
@@ -0,0 +1,1487 @@
+/* -*- Mode: C++; tab-width: 20; 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 "nsProfiler.h"
+
+#include <fstream>
+#include <limits>
+#include <sstream>
+#include <string>
+#include <utility>
+
+#include "GeckoProfiler.h"
+#include "ProfilerControl.h"
+#include "ProfilerParent.h"
+#include "js/Array.h" // JS::NewArrayObject
+#include "js/JSON.h"
+#include "js/PropertyAndElement.h" // JS_SetElement
+#include "js/Value.h"
+#include "json/json.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/JSONStringWriteFuncs.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/Services.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/Preferences.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsILoadContext.h"
+#include "nsIWebNavigation.h"
+#include "nsProfilerStartParams.h"
+#include "nsProxyRelease.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "platform.h"
+#include "shared-libraries.h"
+#include "zlib.h"
+
+#ifndef ANDROID
+# include <cstdio>
+#else
+# include <android/log.h>
+#endif
+
+using namespace mozilla;
+
+using dom::AutoJSAPI;
+using dom::Promise;
+using std::string;
+
+static constexpr size_t scLengthMax = size_t(JS::MaxStringLength);
+// Used when trying to add more JSON data, to account for the extra space needed
+// for the log and to close the profile.
+static constexpr size_t scLengthAccumulationThreshold = scLengthMax - 16 * 1024;
+
+NS_IMPL_ISUPPORTS(nsProfiler, nsIProfiler)
+
+nsProfiler::nsProfiler() : mGathering(false) {}
+
+nsProfiler::~nsProfiler() {
+ if (mSymbolTableThread) {
+ mSymbolTableThread->Shutdown();
+ }
+ ResetGathering(NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
+}
+
+nsresult nsProfiler::Init() { return NS_OK; }
+
+template <typename JsonLogObjectUpdater>
+void nsProfiler::Log(JsonLogObjectUpdater&& aJsonLogObjectUpdater) {
+ if (mGatheringLog) {
+ MOZ_ASSERT(mGatheringLog->isObject());
+ std::forward<JsonLogObjectUpdater>(aJsonLogObjectUpdater)(*mGatheringLog);
+ MOZ_ASSERT(mGatheringLog->isObject());
+ }
+}
+
+template <typename JsonArrayAppender>
+void nsProfiler::LogEvent(JsonArrayAppender&& aJsonArrayAppender) {
+ Log([&](Json::Value& aRoot) {
+ Json::Value& events = aRoot[Json::StaticString{"events"}];
+ if (!events.isArray()) {
+ events = Json::Value{Json::arrayValue};
+ }
+ Json::Value newEvent{Json::arrayValue};
+ newEvent.append(ProfilingLog::Timestamp());
+ std::forward<JsonArrayAppender>(aJsonArrayAppender)(newEvent);
+ MOZ_ASSERT(newEvent.isArray());
+ events.append(std::move(newEvent));
+ });
+}
+
+void nsProfiler::LogEventLiteralString(const char* aEventString) {
+ LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(Json::StaticString{aEventString});
+ });
+}
+
+static nsresult FillVectorFromStringArray(Vector<const char*>& aVector,
+ const nsTArray<nsCString>& aArray) {
+ if (NS_WARN_IF(!aVector.reserve(aArray.Length()))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ for (auto& entry : aArray) {
+ aVector.infallibleAppend(entry.get());
+ }
+ return NS_OK;
+}
+
+// Given a PromiseReturningFunction: () -> GenericPromise,
+// run the function, and return a JS Promise (through aPromise) that will be
+// resolved when the function's GenericPromise gets resolved.
+template <typename PromiseReturningFunction>
+static nsresult RunFunctionAndConvertPromise(
+ JSContext* aCx, Promise** aPromise,
+ PromiseReturningFunction&& aPromiseReturningFunction) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aCx)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!globalObject)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(globalObject, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ std::forward<PromiseReturningFunction>(aPromiseReturningFunction)()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [promise](GenericPromise::ResolveOrRejectValue&&) {
+ promise->MaybeResolveWithUndefined();
+ });
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::StartProfiler(uint32_t aEntries, double aInterval,
+ const nsTArray<nsCString>& aFeatures,
+ const nsTArray<nsCString>& aFilters,
+ uint64_t aActiveTabID, double aDuration,
+ JSContext* aCx, Promise** aPromise) {
+ ResetGathering(NS_ERROR_DOM_ABORT_ERR);
+
+ Vector<const char*> featureStringVector;
+ nsresult rv = FillVectorFromStringArray(featureStringVector, aFeatures);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ uint32_t features = ParseFeaturesFromStringArray(
+ featureStringVector.begin(), featureStringVector.length());
+ Maybe<double> duration = aDuration > 0.0 ? Some(aDuration) : Nothing();
+
+ Vector<const char*> filterStringVector;
+ rv = FillVectorFromStringArray(filterStringVector, aFilters);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return RunFunctionAndConvertPromise(aCx, aPromise, [&]() {
+ return profiler_start(PowerOfTwo32(aEntries), aInterval, features,
+ filterStringVector.begin(),
+ filterStringVector.length(), aActiveTabID, duration);
+ });
+}
+
+NS_IMETHODIMP
+nsProfiler::StopProfiler(JSContext* aCx, Promise** aPromise) {
+ ResetGathering(NS_ERROR_DOM_ABORT_ERR);
+ return RunFunctionAndConvertPromise(aCx, aPromise,
+ []() { return profiler_stop(); });
+}
+
+NS_IMETHODIMP
+nsProfiler::IsPaused(bool* aIsPaused) {
+ *aIsPaused = profiler_is_paused();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::Pause(JSContext* aCx, Promise** aPromise) {
+ return RunFunctionAndConvertPromise(aCx, aPromise,
+ []() { return profiler_pause(); });
+}
+
+NS_IMETHODIMP
+nsProfiler::Resume(JSContext* aCx, Promise** aPromise) {
+ return RunFunctionAndConvertPromise(aCx, aPromise,
+ []() { return profiler_resume(); });
+}
+
+NS_IMETHODIMP
+nsProfiler::IsSamplingPaused(bool* aIsSamplingPaused) {
+ *aIsSamplingPaused = profiler_is_sampling_paused();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::PauseSampling(JSContext* aCx, Promise** aPromise) {
+ return RunFunctionAndConvertPromise(
+ aCx, aPromise, []() { return profiler_pause_sampling(); });
+}
+
+NS_IMETHODIMP
+nsProfiler::ResumeSampling(JSContext* aCx, Promise** aPromise) {
+ return RunFunctionAndConvertPromise(
+ aCx, aPromise, []() { return profiler_resume_sampling(); });
+}
+
+NS_IMETHODIMP
+nsProfiler::ClearAllPages() {
+ profiler_clear_all_pages();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::WaitOnePeriodicSampling(JSContext* aCx, Promise** aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aCx)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!globalObject)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(globalObject, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ // The callback cannot officially own the promise RefPtr directly, because
+ // `Promise` doesn't support multi-threading, and the callback could destroy
+ // the promise in the sampler thread.
+ // `nsMainThreadPtrHandle` ensures that the promise can only be destroyed on
+ // the main thread. And the invocation from the Sampler thread immediately
+ // dispatches a task back to the main thread, to resolve/reject the promise.
+ // The lambda needs to be `mutable`, to allow moving-from
+ // `promiseHandleInSampler`.
+ if (!profiler_callback_after_sampling(
+ [promiseHandleInSampler = nsMainThreadPtrHandle<Promise>(
+ new nsMainThreadPtrHolder<Promise>(
+ "WaitOnePeriodicSampling promise for Sampler", promise))](
+ SamplingState aSamplingState) mutable {
+ SchedulerGroup::Dispatch(
+ TaskCategory::Other,
+ NS_NewRunnableFunction(
+ "nsProfiler::WaitOnePeriodicSampling result on main thread",
+ [promiseHandleInMT = std::move(promiseHandleInSampler),
+ aSamplingState]() mutable {
+ switch (aSamplingState) {
+ case SamplingState::JustStopped:
+ case SamplingState::SamplingPaused:
+ promiseHandleInMT->MaybeReject(NS_ERROR_FAILURE);
+ break;
+
+ case SamplingState::NoStackSamplingCompleted:
+ case SamplingState::SamplingCompleted:
+ // The parent process has succesfully done a sampling,
+ // check the child processes (if any).
+ ProfilerParent::WaitOnePeriodicSampling()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [promiseHandleInMT =
+ std::move(promiseHandleInMT)](
+ GenericPromise::ResolveOrRejectValue&&) {
+ promiseHandleInMT->MaybeResolveWithUndefined();
+ });
+ break;
+
+ default:
+ MOZ_ASSERT(false, "Unexpected SamplingState value");
+ promiseHandleInMT->MaybeReject(
+ NS_ERROR_DOM_UNKNOWN_ERR);
+ break;
+ }
+ }));
+ })) {
+ // Callback was not added (e.g., profiler is not running) and will never be
+ // invoked, so we need to resolve the promise here.
+ promise->MaybeReject(NS_ERROR_DOM_UNKNOWN_ERR);
+ }
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::GetProfile(double aSinceTime, char** aProfile) {
+ mozilla::UniquePtr<char[]> profile = profiler_get_profile(aSinceTime);
+ *aProfile = profile.release();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::GetSharedLibraries(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ JS::Rooted<JS::Value> val(aCx);
+ {
+ JSONStringWriteFunc<nsCString> buffer;
+ JSONWriter w(buffer, JSONWriter::SingleLineStyle);
+ w.StartArrayElement();
+ SharedLibraryInfo sharedLibraryInfo = SharedLibraryInfo::GetInfoForSelf();
+ sharedLibraryInfo.SortByAddress();
+ AppendSharedLibraries(w, sharedLibraryInfo);
+ w.EndArray();
+ NS_ConvertUTF8toUTF16 buffer16(buffer.StringCRef());
+ MOZ_ALWAYS_TRUE(JS_ParseJSON(aCx,
+ static_cast<const char16_t*>(buffer16.get()),
+ buffer16.Length(), &val));
+ }
+ JS::Rooted<JSObject*> obj(aCx, &val.toObject());
+ if (!obj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*obj);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::GetActiveConfiguration(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ JS::Rooted<JS::Value> jsValue(aCx);
+ {
+ JSONStringWriteFunc<nsCString> buffer;
+ JSONWriter writer(buffer, JSONWriter::SingleLineStyle);
+ profiler_write_active_configuration(writer);
+ NS_ConvertUTF8toUTF16 buffer16(buffer.StringCRef());
+ MOZ_ALWAYS_TRUE(JS_ParseJSON(aCx,
+ static_cast<const char16_t*>(buffer16.get()),
+ buffer16.Length(), &jsValue));
+ }
+ if (jsValue.isNull()) {
+ aResult.setNull();
+ } else {
+ JS::Rooted<JSObject*> obj(aCx, &jsValue.toObject());
+ if (!obj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*obj);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::DumpProfileToFile(const char* aFilename) {
+ profiler_save_profile_to_file(aFilename);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::GetProfileData(double aSinceTime, JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ mozilla::UniquePtr<char[]> profile = profiler_get_profile(aSinceTime);
+ if (!profile) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_ConvertUTF8toUTF16 js_string(nsDependentCString(profile.get()));
+ auto profile16 = static_cast<const char16_t*>(js_string.get());
+
+ JS::Rooted<JS::Value> val(aCx);
+ MOZ_ALWAYS_TRUE(JS_ParseJSON(aCx, profile16, js_string.Length(), &val));
+
+ aResult.set(val);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::GetProfileDataAsync(double aSinceTime, JSContext* aCx,
+ Promise** aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!profiler_is_active()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_WARN_IF(!aCx)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!globalObject)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(globalObject, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ StartGathering(aSinceTime)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [promise](const mozilla::ProfileAndAdditionalInformation& aResult) {
+ AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) {
+ // We're really hosed if we can't get a JS context for some
+ // reason.
+ promise->MaybeReject(NS_ERROR_DOM_UNKNOWN_ERR);
+ return;
+ }
+
+ JSContext* cx = jsapi.cx();
+
+ // Now parse the JSON so that we resolve with a JS Object.
+ JS::Rooted<JS::Value> val(cx);
+ {
+ NS_ConvertUTF8toUTF16 js_string(aResult.mProfile);
+ if (!JS_ParseJSON(cx,
+ static_cast<const char16_t*>(js_string.get()),
+ js_string.Length(), &val)) {
+ if (!jsapi.HasException()) {
+ promise->MaybeReject(NS_ERROR_DOM_UNKNOWN_ERR);
+ } else {
+ JS::Rooted<JS::Value> exn(cx);
+ DebugOnly<bool> gotException = jsapi.StealException(&exn);
+ MOZ_ASSERT(gotException);
+
+ jsapi.ClearException();
+ promise->MaybeReject(exn);
+ }
+ } else {
+ promise->MaybeResolve(val);
+ }
+ }
+ },
+ [promise](nsresult aRv) { promise->MaybeReject(aRv); });
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::GetProfileDataAsArrayBuffer(double aSinceTime, JSContext* aCx,
+ Promise** aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!profiler_is_active()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_WARN_IF(!aCx)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!globalObject)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(globalObject, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ StartGathering(aSinceTime)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [promise](const mozilla::ProfileAndAdditionalInformation& aResult) {
+ AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) {
+ // We're really hosed if we can't get a JS context for some
+ // reason.
+ promise->MaybeReject(NS_ERROR_DOM_UNKNOWN_ERR);
+ return;
+ }
+
+ JSContext* cx = jsapi.cx();
+ JSObject* typedArray = dom::ArrayBuffer::Create(
+ cx, aResult.mProfile.Length(),
+ reinterpret_cast<const uint8_t*>(aResult.mProfile.Data()));
+ if (typedArray) {
+ JS::Rooted<JS::Value> val(cx, JS::ObjectValue(*typedArray));
+ promise->MaybeResolve(val);
+ } else {
+ promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
+ }
+ },
+ [promise](nsresult aRv) { promise->MaybeReject(aRv); });
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+nsresult CompressString(const nsCString& aString,
+ FallibleTArray<uint8_t>& aOutBuff) {
+ // Compress a buffer via zlib (as with `compress()`), but emit a
+ // gzip header as well. Like `compress()`, this is limited to 4GB in
+ // size, but that shouldn't be an issue for our purposes.
+ uLongf outSize = compressBound(aString.Length());
+ if (!aOutBuff.SetLength(outSize, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ int zerr;
+ z_stream stream;
+ stream.zalloc = nullptr;
+ stream.zfree = nullptr;
+ stream.opaque = nullptr;
+ stream.next_out = (Bytef*)aOutBuff.Elements();
+ stream.avail_out = aOutBuff.Length();
+ stream.next_in = (z_const Bytef*)aString.Data();
+ stream.avail_in = aString.Length();
+
+ // A windowBits of 31 is the default (15) plus 16 for emitting a
+ // gzip header; a memLevel of 8 is the default.
+ zerr =
+ deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
+ /* windowBits */ 31, /* memLevel */ 8, Z_DEFAULT_STRATEGY);
+ if (zerr != Z_OK) {
+ return NS_ERROR_FAILURE;
+ }
+
+ zerr = deflate(&stream, Z_FINISH);
+ outSize = stream.total_out;
+ deflateEnd(&stream);
+
+ if (zerr != Z_STREAM_END) {
+ return NS_ERROR_FAILURE;
+ }
+
+ aOutBuff.TruncateLength(outSize);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::GetProfileDataAsGzippedArrayBuffer(double aSinceTime,
+ JSContext* aCx,
+ Promise** aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!profiler_is_active()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_WARN_IF(!aCx)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!globalObject)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(globalObject, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ StartGathering(aSinceTime)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [promise](const mozilla::ProfileAndAdditionalInformation& aResult) {
+ AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) {
+ // We're really hosed if we can't get a JS context for some
+ // reason.
+ promise->MaybeReject(NS_ERROR_DOM_UNKNOWN_ERR);
+ return;
+ }
+
+ FallibleTArray<uint8_t> outBuff;
+ nsresult result = CompressString(aResult.mProfile, outBuff);
+
+ if (result != NS_OK) {
+ promise->MaybeReject(result);
+ return;
+ }
+
+ JSContext* cx = jsapi.cx();
+ // Get the profile typedArray.
+ JSObject* typedArray = dom::ArrayBuffer::Create(
+ cx, outBuff.Length(), outBuff.Elements());
+ if (!typedArray) {
+ promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ JS::Rooted<JS::Value> typedArrayValue(cx,
+ JS::ObjectValue(*typedArray));
+ // Get the additional information object.
+ JS::Rooted<JS::Value> additionalInfoVal(cx);
+ if (aResult.mAdditionalInformation.isSome()) {
+ aResult.mAdditionalInformation->ToJSValue(cx, &additionalInfoVal);
+ } else {
+ additionalInfoVal.setUndefined();
+ }
+
+ // Create the return object.
+ JS::Rooted<JSObject*> resultObj(cx, JS_NewPlainObject(cx));
+ JS_SetProperty(cx, resultObj, "profile", typedArrayValue);
+ JS_SetProperty(cx, resultObj, "additionalInformation",
+ additionalInfoVal);
+ promise->MaybeResolve(resultObj);
+ },
+ [promise](nsresult aRv) { promise->MaybeReject(aRv); });
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::DumpProfileToFileAsync(const nsACString& aFilename,
+ double aSinceTime, JSContext* aCx,
+ Promise** aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!profiler_is_active()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_WARN_IF(!aCx)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!globalObject)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(globalObject, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ nsCString filename(aFilename);
+
+ StartGathering(aSinceTime)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [filename,
+ promise](const mozilla::ProfileAndAdditionalInformation& aResult) {
+ if (aResult.mProfile.Length() >=
+ size_t(std::numeric_limits<std::streamsize>::max())) {
+ promise->MaybeReject(NS_ERROR_FILE_TOO_BIG);
+ return;
+ }
+
+ std::ofstream stream;
+ stream.open(filename.get());
+ if (!stream.is_open()) {
+ promise->MaybeReject(NS_ERROR_FILE_UNRECOGNIZED_PATH);
+ return;
+ }
+
+ stream.write(aResult.mProfile.get(),
+ std::streamsize(aResult.mProfile.Length()));
+ stream.close();
+
+ promise->MaybeResolveWithUndefined();
+ },
+ [promise](nsresult aRv) { promise->MaybeReject(aRv); });
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::GetSymbolTable(const nsACString& aDebugPath,
+ const nsACString& aBreakpadID, JSContext* aCx,
+ Promise** aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!aCx)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsIGlobalObject* globalObject =
+ xpc::NativeGlobal(JS::CurrentGlobalOrNull(aCx));
+
+ if (NS_WARN_IF(!globalObject)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(globalObject, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ GetSymbolTableMozPromise(aDebugPath, aBreakpadID)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [promise](const SymbolTable& aSymbolTable) {
+ AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) {
+ // We're really hosed if we can't get a JS context for some
+ // reason.
+ promise->MaybeReject(NS_ERROR_DOM_UNKNOWN_ERR);
+ return;
+ }
+
+ JSContext* cx = jsapi.cx();
+
+ JS::Rooted<JSObject*> addrsArray(
+ cx, dom::Uint32Array::Create(cx, aSymbolTable.mAddrs.Length(),
+ aSymbolTable.mAddrs.Elements()));
+ JS::Rooted<JSObject*> indexArray(
+ cx, dom::Uint32Array::Create(cx, aSymbolTable.mIndex.Length(),
+ aSymbolTable.mIndex.Elements()));
+ JS::Rooted<JSObject*> bufferArray(
+ cx, dom::Uint8Array::Create(cx, aSymbolTable.mBuffer.Length(),
+ aSymbolTable.mBuffer.Elements()));
+
+ if (addrsArray && indexArray && bufferArray) {
+ JS::Rooted<JSObject*> tuple(cx, JS::NewArrayObject(cx, 3));
+ JS_SetElement(cx, tuple, 0, addrsArray);
+ JS_SetElement(cx, tuple, 1, indexArray);
+ JS_SetElement(cx, tuple, 2, bufferArray);
+ promise->MaybeResolve(tuple);
+ } else {
+ promise->MaybeReject(NS_ERROR_FAILURE);
+ }
+ },
+ [promise](nsresult aRv) { promise->MaybeReject(aRv); });
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::GetElapsedTime(double* aElapsedTime) {
+ *aElapsedTime = profiler_time();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::IsActive(bool* aIsActive) {
+ *aIsActive = profiler_is_active();
+ return NS_OK;
+}
+
+static void GetArrayOfStringsForFeatures(uint32_t aFeatures,
+ nsTArray<nsCString>& aFeatureList) {
+#define COUNT_IF_SET(n_, str_, Name_, desc_) \
+ if (ProfilerFeature::Has##Name_(aFeatures)) { \
+ len++; \
+ }
+
+ // Count the number of features in use.
+ uint32_t len = 0;
+ PROFILER_FOR_EACH_FEATURE(COUNT_IF_SET)
+
+#undef COUNT_IF_SET
+
+ aFeatureList.SetCapacity(len);
+
+#define DUP_IF_SET(n_, str_, Name_, desc_) \
+ if (ProfilerFeature::Has##Name_(aFeatures)) { \
+ aFeatureList.AppendElement(str_); \
+ }
+
+ // Insert the strings for the features in use.
+ PROFILER_FOR_EACH_FEATURE(DUP_IF_SET)
+
+#undef DUP_IF_SET
+}
+
+NS_IMETHODIMP
+nsProfiler::GetFeatures(nsTArray<nsCString>& aFeatureList) {
+ uint32_t features = profiler_get_available_features();
+ GetArrayOfStringsForFeatures(features, aFeatureList);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::GetAllFeatures(nsTArray<nsCString>& aFeatureList) {
+ GetArrayOfStringsForFeatures((uint32_t)-1, aFeatureList);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfiler::GetBufferInfo(uint32_t* aCurrentPosition, uint32_t* aTotalSize,
+ uint32_t* aGeneration) {
+ MOZ_ASSERT(aCurrentPosition);
+ MOZ_ASSERT(aTotalSize);
+ MOZ_ASSERT(aGeneration);
+ Maybe<ProfilerBufferInfo> info = profiler_get_buffer_info();
+ if (info) {
+ *aCurrentPosition = info->mRangeEnd % info->mEntryCount;
+ *aTotalSize = info->mEntryCount;
+ *aGeneration = info->mRangeEnd / info->mEntryCount;
+ } else {
+ *aCurrentPosition = 0;
+ *aTotalSize = 0;
+ *aGeneration = 0;
+ }
+ return NS_OK;
+}
+
+bool nsProfiler::SendProgressRequest(PendingProfile& aPendingProfile) {
+ RefPtr<ProfilerParent::SingleProcessProgressPromise> progressPromise =
+ ProfilerParent::RequestGatherProfileProgress(aPendingProfile.childPid);
+ if (!progressPromise) {
+ LOG("RequestGatherProfileProgress(%u) -> null!",
+ unsigned(aPendingProfile.childPid));
+ LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(
+ Json::StaticString{"Failed to send progress request to pid:"});
+ aEvent.append(Json::Value::UInt64(aPendingProfile.childPid));
+ });
+ // Failed to send request.
+ return false;
+ }
+
+ DEBUG_LOG("RequestGatherProfileProgress(%u) sent...",
+ unsigned(aPendingProfile.childPid));
+ LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(Json::StaticString{"Requested progress from pid:"});
+ aEvent.append(Json::Value::UInt64(aPendingProfile.childPid));
+ });
+ aPendingProfile.lastProgressRequest = TimeStamp::Now();
+ progressPromise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self = RefPtr<nsProfiler>(this),
+ childPid = aPendingProfile.childPid](GatherProfileProgress&& aResult) {
+ if (!self->mGathering) {
+ return;
+ }
+ PendingProfile* pendingProfile = self->GetPendingProfile(childPid);
+ DEBUG_LOG(
+ "RequestGatherProfileProgress(%u) response: %.2f '%s' "
+ "(%u were pending, %s %u)",
+ unsigned(childPid),
+ ProportionValue::FromUnderlyingType(
+ aResult.progressProportionValueUnderlyingType())
+ .ToDouble() *
+ 100.0,
+ aResult.progressLocation().Data(),
+ unsigned(self->mPendingProfiles.length()),
+ pendingProfile ? "including" : "excluding", unsigned(childPid));
+ self->LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(
+ Json::StaticString{"Got response from pid, with progress:"});
+ aEvent.append(Json::Value::UInt64(childPid));
+ aEvent.append(
+ Json::Value{ProportionValue::FromUnderlyingType(
+ aResult.progressProportionValueUnderlyingType())
+ .ToDouble() *
+ 100.0});
+ });
+ if (pendingProfile) {
+ // We have a progress report for a still-pending profile.
+ pendingProfile->lastProgressResponse = TimeStamp::Now();
+ // Has it actually made progress?
+ if (aResult.progressProportionValueUnderlyingType() !=
+ pendingProfile->progressProportion.ToUnderlyingType()) {
+ pendingProfile->lastProgressChange =
+ pendingProfile->lastProgressResponse;
+ pendingProfile->progressProportion =
+ ProportionValue::FromUnderlyingType(
+ aResult.progressProportionValueUnderlyingType());
+ pendingProfile->progressLocation = aResult.progressLocation();
+ self->RestartGatheringTimer();
+ }
+ }
+ },
+ [self = RefPtr<nsProfiler>(this), childPid = aPendingProfile.childPid](
+ ipc::ResponseRejectReason&& aReason) {
+ if (!self->mGathering) {
+ return;
+ }
+ PendingProfile* pendingProfile = self->GetPendingProfile(childPid);
+ LOG("RequestGatherProfileProgress(%u) rejection: %d "
+ "(%u were pending, %s %u)",
+ unsigned(childPid), (int)aReason,
+ unsigned(self->mPendingProfiles.length()),
+ pendingProfile ? "including" : "excluding", unsigned(childPid));
+ self->LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(Json::StaticString{
+ "Got progress request rejection from pid, with reason:"});
+ aEvent.append(Json::Value::UInt64(childPid));
+ aEvent.append(Json::Value::UInt{static_cast<unsigned>(aReason)});
+ });
+ if (pendingProfile) {
+ // Failure response, assume the child process is gone.
+ MOZ_ASSERT(self->mPendingProfiles.begin() <= pendingProfile &&
+ pendingProfile < self->mPendingProfiles.end());
+ self->mPendingProfiles.erase(pendingProfile);
+ if (self->mPendingProfiles.empty()) {
+ // We've got all of the async profiles now. Let's finish off the
+ // profile and resolve the Promise.
+ self->FinishGathering();
+ }
+ }
+ });
+ return true;
+}
+
+/* static */ void nsProfiler::GatheringTimerCallback(nsITimer* aTimer,
+ void* aClosure) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIProfiler> profiler(
+ do_GetService("@mozilla.org/tools/profiler;1"));
+ if (!profiler) {
+ // No (more) profiler service.
+ return;
+ }
+ nsProfiler* self = static_cast<nsProfiler*>(profiler.get());
+ if (self != aClosure) {
+ // Different service object!?
+ return;
+ }
+ if (aTimer != self->mGatheringTimer) {
+ // This timer was cancelled after this callback was queued.
+ return;
+ }
+
+ bool progressWasMade = false;
+
+ // Going backwards, it's easier and cheaper to erase elements if needed.
+ for (auto iPlus1 = self->mPendingProfiles.length(); iPlus1 != 0; --iPlus1) {
+ PendingProfile& pendingProfile = self->mPendingProfiles[iPlus1 - 1];
+
+ bool needToSendProgressRequest = false;
+ if (pendingProfile.lastProgressRequest.IsNull()) {
+ DEBUG_LOG("GatheringTimerCallback() - child %u: No data yet",
+ unsigned(pendingProfile.childPid));
+ // First time going through the list, send an initial progress request.
+ needToSendProgressRequest = true;
+ // We pretend that progress was made, so we don't give up yet.
+ progressWasMade = true;
+ } else if (pendingProfile.lastProgressResponse.IsNull()) {
+ LOG("GatheringTimerCallback() - child %u: Waiting for first response",
+ unsigned(pendingProfile.childPid));
+ // Still waiting for the first response, no progress made here, don't send
+ // another request.
+ } else if (pendingProfile.lastProgressResponse <=
+ pendingProfile.lastProgressRequest) {
+ LOG("GatheringTimerCallback() - child %u: Waiting for response",
+ unsigned(pendingProfile.childPid));
+ // Still waiting for a response to the last request, no progress made
+ // here, don't send another request.
+ } else if (pendingProfile.lastProgressChange.IsNull()) {
+ LOG("GatheringTimerCallback() - child %u: Still waiting for first change",
+ unsigned(pendingProfile.childPid));
+ // Still waiting for the first change, no progress made here, but send a
+ // new request.
+ needToSendProgressRequest = true;
+ } else if (pendingProfile.lastProgressRequest <
+ pendingProfile.lastProgressChange) {
+ DEBUG_LOG("GatheringTimerCallback() - child %u: Recent change",
+ unsigned(pendingProfile.childPid));
+ // We have a recent change, progress was made.
+ needToSendProgressRequest = true;
+ progressWasMade = true;
+ } else {
+ LOG("GatheringTimerCallback() - child %u: No recent change",
+ unsigned(pendingProfile.childPid));
+ needToSendProgressRequest = true;
+ }
+
+ // And send a new progress request.
+ if (needToSendProgressRequest) {
+ if (!self->SendProgressRequest(pendingProfile)) {
+ // Failed to even send the request, consider this process gone.
+ self->mPendingProfiles.erase(&pendingProfile);
+ LOG("... Failed to send progress request");
+ } else {
+ DEBUG_LOG("... Sent progress request");
+ }
+ } else {
+ DEBUG_LOG("... No progress request");
+ }
+ }
+
+ if (self->mPendingProfiles.empty()) {
+ // We've got all of the async profiles now. Let's finish off the profile
+ // and resolve the Promise.
+ self->FinishGathering();
+ return;
+ }
+
+ // Not finished yet.
+
+ if (progressWasMade) {
+ // We made some progress, just restart the timer.
+ DEBUG_LOG("GatheringTimerCallback() - Progress made, restart timer");
+ self->RestartGatheringTimer();
+ return;
+ }
+
+ DEBUG_LOG("GatheringTimerCallback() - Timeout!");
+ self->mGatheringTimer = nullptr;
+ if (!profiler_is_active() || !self->mGathering) {
+ // Not gathering anymore.
+ return;
+ }
+ self->LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(Json::StaticString{
+ "No progress made recently, giving up; pending pids:"});
+ for (const PendingProfile& pendingProfile : self->mPendingProfiles) {
+ aEvent.append(Json::Value::UInt64(pendingProfile.childPid));
+ }
+ });
+ NS_WARNING("Profiler failed to gather profiles from all sub-processes");
+ // We have really reached a timeout while gathering, finish now.
+ // TODO: Add information about missing processes.
+ self->FinishGathering();
+}
+
+void nsProfiler::RestartGatheringTimer() {
+ if (mGatheringTimer) {
+ uint32_t delayMs = 0;
+ const nsresult r = mGatheringTimer->GetDelay(&delayMs);
+ mGatheringTimer->Cancel();
+ if (NS_FAILED(r) || delayMs == 0 ||
+ NS_FAILED(mGatheringTimer->InitWithNamedFuncCallback(
+ GatheringTimerCallback, this, delayMs,
+ nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY,
+ "nsProfilerGatheringTimer"))) {
+ // Can't restart the timer, so we can't wait any longer.
+ FinishGathering();
+ }
+ }
+}
+
+nsProfiler::PendingProfile* nsProfiler::GetPendingProfile(
+ base::ProcessId aChildPid) {
+ for (PendingProfile& pendingProfile : mPendingProfiles) {
+ if (pendingProfile.childPid == aChildPid) {
+ return &pendingProfile;
+ }
+ }
+ return nullptr;
+}
+
+void nsProfiler::GatheredOOPProfile(
+ base::ProcessId aChildPid, const nsACString& aProfile,
+ mozilla::Maybe<ProfileGenerationAdditionalInformation>&&
+ aAdditionalInformation) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+ if (!profiler_is_active()) {
+ return;
+ }
+
+ if (!mGathering) {
+ // If we're not actively gathering, then we don't actually care that we
+ // gathered a profile here. This can happen for processes that exit while
+ // profiling.
+ return;
+ }
+
+ MOZ_RELEASE_ASSERT(mWriter.isSome(),
+ "Should always have a writer if mGathering is true");
+
+ // Combine all the additional information into a single struct.
+ if (aAdditionalInformation.isSome()) {
+ mProfileGenerationAdditionalInformation->Append(
+ std::move(*aAdditionalInformation));
+ }
+
+ if (!aProfile.IsEmpty()) {
+ if (mWriter->ChunkedWriteFunc().Length() + aProfile.Length() <
+ scLengthAccumulationThreshold) {
+ // TODO: Remove PromiseFlatCString, see bug 1657033.
+ mWriter->Splice(PromiseFlatCString(aProfile));
+ } else {
+ LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(
+ Json::StaticString{"Discarded child profile that would make the "
+ "full profile too big, pid and size:"});
+ aEvent.append(Json::Value::UInt64(aChildPid));
+ aEvent.append(Json::Value::UInt64{aProfile.Length()});
+ });
+ }
+ }
+
+ if (PendingProfile* pendingProfile = GetPendingProfile(aChildPid);
+ pendingProfile) {
+ mPendingProfiles.erase(pendingProfile);
+
+ if (mPendingProfiles.empty()) {
+ // We've got all of the async profiles now. Let's finish off the profile
+ // and resolve the Promise.
+ FinishGathering();
+ }
+ }
+
+ // Not finished yet, restart the timer to let any remaining child enough time
+ // to do their profile-streaming.
+ RestartGatheringTimer();
+}
+
+RefPtr<nsProfiler::GatheringPromiseAndroid>
+nsProfiler::GetProfileDataAsGzippedArrayBufferAndroid(double aSinceTime) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!profiler_is_active()) {
+ return GatheringPromiseAndroid::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ return StartGathering(aSinceTime)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [](const mozilla::ProfileAndAdditionalInformation& aResult) {
+ FallibleTArray<uint8_t> outBuff;
+ nsresult result = CompressString(aResult.mProfile, outBuff);
+ if (result != NS_OK) {
+ return GatheringPromiseAndroid::CreateAndReject(result, __func__);
+ }
+ return GatheringPromiseAndroid::CreateAndResolve(std::move(outBuff),
+ __func__);
+ },
+ [](nsresult aRv) {
+ return GatheringPromiseAndroid::CreateAndReject(aRv, __func__);
+ });
+}
+
+RefPtr<nsProfiler::GatheringPromise> nsProfiler::StartGathering(
+ double aSinceTime) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+ if (mGathering) {
+ // If we're already gathering, return a rejected promise - this isn't
+ // going to end well.
+ return GatheringPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__);
+ }
+
+ mGathering = true;
+ mGatheringLog = mozilla::MakeUnique<Json::Value>(Json::objectValue);
+ (*mGatheringLog)[Json::StaticString{
+ "profileGatheringLogBegin" TIMESTAMP_JSON_SUFFIX}] =
+ ProfilingLog::Timestamp();
+
+ if (mGatheringTimer) {
+ mGatheringTimer->Cancel();
+ mGatheringTimer = nullptr;
+ }
+
+ // Start building shared library info starting from the current process.
+ mProfileGenerationAdditionalInformation.emplace(
+ SharedLibraryInfo::GetInfoForSelf());
+
+ // Request profiles from the other processes. This will trigger asynchronous
+ // calls to ProfileGatherer::GatheredOOPProfile as the profiles arrive.
+ //
+ // Do this before the call to profiler_stream_json_for_this_process() because
+ // that call is slow and we want to let the other processes grab their
+ // profiles as soon as possible.
+ nsTArray<ProfilerParent::SingleProcessProfilePromiseAndChildPid> profiles =
+ ProfilerParent::GatherProfiles();
+
+ MOZ_ASSERT(mPendingProfiles.empty());
+ if (!mPendingProfiles.reserve(profiles.Length())) {
+ ResetGathering(NS_ERROR_OUT_OF_MEMORY);
+ return GatheringPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+
+ mFailureLatchSource.emplace();
+ mWriter.emplace(*mFailureLatchSource);
+
+ UniquePtr<ProfilerCodeAddressService> service =
+ profiler_code_address_service_for_presymbolication();
+
+ // Start building up the JSON result and grab the profile from this process.
+ mWriter->Start();
+ auto rv = profiler_stream_json_for_this_process(*mWriter, aSinceTime,
+ /* aIsShuttingDown */ false,
+ service.get());
+ if (rv.isErr()) {
+ // The profiler is inactive. This either means that it was inactive even
+ // at the time that ProfileGatherer::Start() was called, or that it was
+ // stopped on a different thread since that call. Either way, we need to
+ // reject the promise and stop gathering.
+ ResetGathering(NS_ERROR_NOT_AVAILABLE);
+ return GatheringPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__);
+ }
+
+ LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(
+ Json::StaticString{"Generated parent process profile, size:"});
+ aEvent.append(Json::Value::UInt64{mWriter->ChunkedWriteFunc().Length()});
+ });
+
+ mWriter->StartArrayProperty("processes");
+
+ // If we have any process exit profiles, add them immediately.
+ if (Vector<nsCString> exitProfiles = profiler_move_exit_profiles();
+ !exitProfiles.empty()) {
+ for (auto& exitProfile : exitProfiles) {
+ if (!exitProfile.IsEmpty()) {
+ if (exitProfile[0] == '*') {
+ LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(
+ Json::StaticString{"Exit non-profile with error message:"});
+ aEvent.append(exitProfile.Data() + 1);
+ });
+ } else if (mWriter->ChunkedWriteFunc().Length() + exitProfile.Length() <
+ scLengthAccumulationThreshold) {
+ mWriter->Splice(exitProfile);
+ LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(Json::StaticString{"Added exit profile with size:"});
+ aEvent.append(Json::Value::UInt64{exitProfile.Length()});
+ });
+ } else {
+ LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(
+ Json::StaticString{"Discarded an exit profile that would make "
+ "the full profile too big, size:"});
+ aEvent.append(Json::Value::UInt64{exitProfile.Length()});
+ });
+ }
+ }
+ }
+
+ LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(Json::StaticString{
+ "Processed all exit profiles, total size so far:"});
+ aEvent.append(Json::Value::UInt64{mWriter->ChunkedWriteFunc().Length()});
+ });
+ } else {
+ // There are no pending profiles, we're already done.
+ LogEventLiteralString("No exit profiles.");
+ }
+
+ mPromiseHolder.emplace();
+ RefPtr<GatheringPromise> promise = mPromiseHolder->Ensure(__func__);
+
+ // Keep the array property "processes" and the root object in mWriter open
+ // until FinishGathering() is called. As profiles from the other processes
+ // come in, they will be inserted and end up in the right spot.
+ // FinishGathering() will close the array and the root object.
+
+ if (!profiles.IsEmpty()) {
+ // There *are* pending profiles, let's add handlers for their promises.
+
+ // This timeout value is used to monitor progress while gathering child
+ // profiles. The timer will be restarted after we receive a response with
+ // any progress.
+ constexpr uint32_t cMinChildTimeoutS = 1u; // 1 second minimum and default.
+ constexpr uint32_t cMaxChildTimeoutS = 60u; // 1 minute max.
+ uint32_t childTimeoutS = Preferences::GetUint(
+ "devtools.performance.recording.child.timeout_s", cMinChildTimeoutS);
+ if (childTimeoutS < cMinChildTimeoutS) {
+ childTimeoutS = cMinChildTimeoutS;
+ } else if (childTimeoutS > cMaxChildTimeoutS) {
+ childTimeoutS = cMaxChildTimeoutS;
+ }
+ const uint32_t childTimeoutMs = childTimeoutS * PR_MSEC_PER_SEC;
+ Unused << NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mGatheringTimer), GatheringTimerCallback, this,
+ childTimeoutMs, nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY,
+ "nsProfilerGatheringTimer", GetMainThreadSerialEventTarget());
+
+ MOZ_ASSERT(mPendingProfiles.capacity() >= profiles.Length());
+ for (const auto& profile : profiles) {
+ mPendingProfiles.infallibleAppend(PendingProfile{profile.childPid});
+ LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(Json::StaticString{"Waiting for pending profile, pid:"});
+ aEvent.append(Json::Value::UInt64(profile.childPid));
+ });
+ profile.profilePromise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self = RefPtr<nsProfiler>(this), childPid = profile.childPid](
+ IPCProfileAndAdditionalInformation&& aResult) {
+ PendingProfile* pendingProfile = self->GetPendingProfile(childPid);
+ mozilla::ipc::Shmem profileShmem = aResult.profileShmem();
+ LOG("GatherProfile(%u) response: %u bytes (%u were pending, %s %u)",
+ unsigned(childPid), unsigned(profileShmem.Size<char>()),
+ unsigned(self->mPendingProfiles.length()),
+ pendingProfile ? "including" : "excluding", unsigned(childPid));
+ if (profileShmem.IsReadable()) {
+ self->LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(
+ Json::StaticString{"Got profile from pid, with size:"});
+ aEvent.append(Json::Value::UInt64(childPid));
+ aEvent.append(Json::Value::UInt64{profileShmem.Size<char>()});
+ });
+ const nsDependentCSubstring profileString(
+ profileShmem.get<char>(), profileShmem.Size<char>() - 1);
+ if (profileString.IsEmpty() || profileString[0] != '*') {
+ self->GatheredOOPProfile(
+ childPid, profileString,
+ std::move(aResult.additionalInformation()));
+ } else {
+ self->LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(Json::StaticString{
+ "Child non-profile from pid, with error message:"});
+ aEvent.append(Json::Value::UInt64(childPid));
+ aEvent.append(profileString.Data() + 1);
+ });
+ self->GatheredOOPProfile(childPid, ""_ns, Nothing());
+ }
+ } else {
+ // This can happen if the child failed to allocate
+ // the Shmem (or maliciously sent an invalid Shmem).
+ self->LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(Json::StaticString{"Got failure from pid:"});
+ aEvent.append(Json::Value::UInt64(childPid));
+ });
+ self->GatheredOOPProfile(childPid, ""_ns, Nothing());
+ }
+ },
+ [self = RefPtr<nsProfiler>(this),
+ childPid = profile.childPid](ipc::ResponseRejectReason&& aReason) {
+ PendingProfile* pendingProfile = self->GetPendingProfile(childPid);
+ LOG("GatherProfile(%u) rejection: %d (%u were pending, %s %u)",
+ unsigned(childPid), (int)aReason,
+ unsigned(self->mPendingProfiles.length()),
+ pendingProfile ? "including" : "excluding", unsigned(childPid));
+ self->LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(
+ Json::StaticString{"Got rejection from pid, with reason:"});
+ aEvent.append(Json::Value::UInt64(childPid));
+ aEvent.append(Json::Value::UInt{static_cast<unsigned>(aReason)});
+ });
+ self->GatheredOOPProfile(childPid, ""_ns, Nothing());
+ });
+ }
+ } else {
+ // There are no pending profiles, we're already done.
+ LogEventLiteralString("No pending child profiles.");
+ FinishGathering();
+ }
+
+ return promise;
+}
+
+RefPtr<nsProfiler::SymbolTablePromise> nsProfiler::GetSymbolTableMozPromise(
+ const nsACString& aDebugPath, const nsACString& aBreakpadID) {
+ MozPromiseHolder<SymbolTablePromise> promiseHolder;
+ RefPtr<SymbolTablePromise> promise = promiseHolder.Ensure(__func__);
+
+ if (!mSymbolTableThread) {
+ nsresult rv = NS_NewNamedThread("ProfSymbolTable",
+ getter_AddRefs(mSymbolTableThread));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ promiseHolder.Reject(NS_ERROR_FAILURE, __func__);
+ return promise;
+ }
+ }
+
+ nsresult rv = mSymbolTableThread->Dispatch(NS_NewRunnableFunction(
+ "nsProfiler::GetSymbolTableMozPromise runnable on ProfSymbolTable thread",
+ [promiseHolder = std::move(promiseHolder),
+ debugPath = nsCString(aDebugPath),
+ breakpadID = nsCString(aBreakpadID)]() mutable {
+ AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING("profiler_get_symbol_table",
+ OTHER, debugPath);
+ SymbolTable symbolTable;
+ bool succeeded = profiler_get_symbol_table(
+ debugPath.get(), breakpadID.get(), &symbolTable);
+ if (succeeded) {
+ promiseHolder.Resolve(std::move(symbolTable), __func__);
+ } else {
+ promiseHolder.Reject(NS_ERROR_FAILURE, __func__);
+ }
+ }));
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // Get-symbol task was not dispatched and therefore won't fulfill the
+ // promise, we must reject the promise now.
+ promiseHolder.Reject(NS_ERROR_FAILURE, __func__);
+ }
+
+ return promise;
+}
+
+void nsProfiler::FinishGathering() {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ MOZ_RELEASE_ASSERT(mWriter.isSome());
+ MOZ_RELEASE_ASSERT(mPromiseHolder.isSome());
+ MOZ_RELEASE_ASSERT(mProfileGenerationAdditionalInformation.isSome());
+
+ // Close the "processes" array property.
+ mWriter->EndArray();
+
+ if (mGatheringLog) {
+ LogEvent([&](Json::Value& aEvent) {
+ aEvent.append(Json::StaticString{"Finished gathering, total size:"});
+ aEvent.append(Json::Value::UInt64{mWriter->ChunkedWriteFunc().Length()});
+ });
+ (*mGatheringLog)[Json::StaticString{
+ "profileGatheringLogEnd" TIMESTAMP_JSON_SUFFIX}] =
+ ProfilingLog::Timestamp();
+ mWriter->StartObjectProperty("profileGatheringLog");
+ {
+ nsAutoCString pid;
+ pid.AppendInt(int64_t(profiler_current_process_id().ToNumber()));
+ Json::String logString = ToCompactString(*mGatheringLog);
+ mGatheringLog = nullptr;
+ mWriter->SplicedJSONProperty(pid, logString);
+ }
+ mWriter->EndObject();
+ }
+
+ // Close the root object of the generated JSON.
+ mWriter->End();
+
+ if (const char* failure = mWriter->GetFailure(); failure) {
+#ifndef ANDROID
+ fprintf(stderr, "JSON generation failure: %s", failure);
+#else
+ __android_log_print(ANDROID_LOG_INFO, "GeckoProfiler",
+ "JSON generation failure: %s", failure);
+#endif
+ NS_WARNING("Error during JSON generation, probably OOM.");
+ ResetGathering(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ // And try to resolve the promise with the profile JSON.
+ const size_t len = mWriter->ChunkedWriteFunc().Length();
+ if (len >= scLengthMax) {
+ NS_WARNING("Profile JSON is too big to fit in a string.");
+ ResetGathering(NS_ERROR_FILE_TOO_BIG);
+ return;
+ }
+
+ nsCString result;
+ if (!result.SetLength(len, fallible)) {
+ NS_WARNING("Cannot allocate a string for the Profile JSON.");
+ ResetGathering(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ MOZ_ASSERT(*(result.Data() + len) == '\0',
+ "We expected a null at the end of the string buffer, to be "
+ "rewritten by CopyDataIntoLazilyAllocatedBuffer");
+
+ char* const resultBeginWriting = result.BeginWriting();
+ if (!resultBeginWriting) {
+ NS_WARNING("Cannot access the string to write the Profile JSON.");
+ ResetGathering(NS_ERROR_CACHE_WRITE_ACCESS_DENIED);
+ return;
+ }
+
+ // Here, we have enough space reserved in `result`, starting at
+ // `resultBeginWriting`, copy the JSON profile there.
+ if (!mWriter->ChunkedWriteFunc().CopyDataIntoLazilyAllocatedBuffer(
+ [&](size_t aBufferLen) -> char* {
+ MOZ_RELEASE_ASSERT(aBufferLen == len + 1);
+ return resultBeginWriting;
+ })) {
+ NS_WARNING("Could not copy profile JSON, probably OOM.");
+ ResetGathering(NS_ERROR_FILE_TOO_BIG);
+ return;
+ }
+ MOZ_ASSERT(*(result.Data() + len) == '\0',
+ "We still expected a null at the end of the string buffer");
+
+ mProfileGenerationAdditionalInformation->FinishGathering();
+ mPromiseHolder->Resolve(
+ ProfileAndAdditionalInformation{
+ std::move(result),
+ std::move(*mProfileGenerationAdditionalInformation)},
+ __func__);
+
+ ResetGathering(NS_ERROR_UNEXPECTED);
+}
+
+void nsProfiler::ResetGathering(nsresult aPromiseRejectionIfPending) {
+ // If we have an unfulfilled Promise in flight, we should reject it before
+ // destroying the promise holder.
+ if (mPromiseHolder.isSome()) {
+ mPromiseHolder->RejectIfExists(aPromiseRejectionIfPending, __func__);
+ mPromiseHolder.reset();
+ }
+ mPendingProfiles.clearAndFree();
+ mGathering = false;
+ mGatheringLog = nullptr;
+ if (mGatheringTimer) {
+ mGatheringTimer->Cancel();
+ mGatheringTimer = nullptr;
+ }
+ mWriter.reset();
+ mFailureLatchSource.reset();
+ mProfileGenerationAdditionalInformation.reset();
+}
diff --git a/tools/profiler/gecko/nsProfiler.h b/tools/profiler/gecko/nsProfiler.h
new file mode 100644
index 0000000000..3757df3079
--- /dev/null
+++ b/tools/profiler/gecko/nsProfiler.h
@@ -0,0 +1,117 @@
+/* -*- 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/. */
+
+#ifndef nsProfiler_h
+#define nsProfiler_h
+
+#include "base/process.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/ProfileJSONWriter.h"
+#include "mozilla/ProportionValue.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Vector.h"
+#include "nsIProfiler.h"
+#include "nsITimer.h"
+#include "nsServiceManagerUtils.h"
+#include "ProfilerCodeAddressService.h"
+#include "ProfileAdditionalInformation.h"
+
+namespace Json {
+class Value;
+} // namespace Json
+
+class nsProfiler final : public nsIProfiler {
+ public:
+ nsProfiler();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPROFILER
+
+ nsresult Init();
+
+ static nsProfiler* GetOrCreate() {
+ nsCOMPtr<nsIProfiler> iprofiler =
+ do_GetService("@mozilla.org/tools/profiler;1");
+ return static_cast<nsProfiler*>(iprofiler.get());
+ }
+
+ private:
+ ~nsProfiler();
+
+ using GatheringPromiseAndroid =
+ mozilla::MozPromise<FallibleTArray<uint8_t>, nsresult, true>;
+ using GatheringPromise =
+ mozilla::MozPromise<mozilla::ProfileAndAdditionalInformation, nsresult,
+ false>;
+ using SymbolTablePromise =
+ mozilla::MozPromise<mozilla::SymbolTable, nsresult, true>;
+
+ RefPtr<GatheringPromise> StartGathering(double aSinceTime);
+ void GatheredOOPProfile(
+ base::ProcessId aChildPid, const nsACString& aProfile,
+ mozilla::Maybe<mozilla::ProfileGenerationAdditionalInformation>&&
+ aAdditionalInformation);
+ void FinishGathering();
+ void ResetGathering(nsresult aPromiseRejectionIfPending);
+ static void GatheringTimerCallback(nsITimer* aTimer, void* aClosure);
+ void RestartGatheringTimer();
+
+ RefPtr<SymbolTablePromise> GetSymbolTableMozPromise(
+ const nsACString& aDebugPath, const nsACString& aBreakpadID);
+
+ struct ExitProfile {
+ nsCString mJSON;
+ uint64_t mBufferPositionAtGatherTime;
+ };
+
+ struct PendingProfile {
+ base::ProcessId childPid;
+
+ mozilla::ProportionValue progressProportion;
+ nsCString progressLocation;
+
+ mozilla::TimeStamp lastProgressRequest;
+ mozilla::TimeStamp lastProgressResponse;
+ mozilla::TimeStamp lastProgressChange;
+
+ explicit PendingProfile(base::ProcessId aChildPid) : childPid(aChildPid) {}
+ };
+
+ PendingProfile* GetPendingProfile(base::ProcessId aChildPid);
+ // Returns false if the request could not be sent.
+ bool SendProgressRequest(PendingProfile& aPendingProfile);
+
+ // If the log is active, call aJsonLogObjectUpdater(Json::Value&) on the log's
+ // root object.
+ template <typename JsonLogObjectUpdater>
+ void Log(JsonLogObjectUpdater&& aJsonLogObjectUpdater);
+ // If the log is active, call aJsonArrayAppender(Json::Value&) on a Json
+ // array that already contains a timestamp, and to which event-related
+ // elements may be appended.
+ template <typename JsonArrayAppender>
+ void LogEvent(JsonArrayAppender&& aJsonArrayAppender);
+ void LogEventLiteralString(const char* aEventString);
+
+ // These fields are all related to profile gathering.
+ mozilla::Vector<ExitProfile> mExitProfiles;
+ mozilla::Maybe<mozilla::MozPromiseHolder<GatheringPromise>> mPromiseHolder;
+ nsCOMPtr<nsIThread> mSymbolTableThread;
+ mozilla::Maybe<mozilla::FailureLatchSource> mFailureLatchSource;
+ mozilla::Maybe<SpliceableChunkedJSONWriter> mWriter;
+ mozilla::Maybe<mozilla::ProfileGenerationAdditionalInformation>
+ mProfileGenerationAdditionalInformation;
+ mozilla::Vector<PendingProfile> mPendingProfiles;
+ bool mGathering;
+ nsCOMPtr<nsITimer> mGatheringTimer;
+ // Supplemental log to the profiler's "profilingLog" (which has already been
+ // completed in JSON profiles that are gathered).
+ mozilla::UniquePtr<Json::Value> mGatheringLog;
+};
+
+#endif // nsProfiler_h
diff --git a/tools/profiler/gecko/nsProfilerCIID.h b/tools/profiler/gecko/nsProfilerCIID.h
new file mode 100644
index 0000000000..3df44596b1
--- /dev/null
+++ b/tools/profiler/gecko/nsProfilerCIID.h
@@ -0,0 +1,16 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsProfilerCIID_h__
+#define nsProfilerCIID_h__
+
+#define NS_PROFILER_CID \
+ { \
+ 0x25db9b8e, 0x8123, 0x4de1, { \
+ 0xb6, 0x6d, 0x8b, 0xbb, 0xed, 0xf2, 0xcd, 0xf4 \
+ } \
+ }
+
+#endif
diff --git a/tools/profiler/gecko/nsProfilerStartParams.cpp b/tools/profiler/gecko/nsProfilerStartParams.cpp
new file mode 100644
index 0000000000..dd7c3f4ab7
--- /dev/null
+++ b/tools/profiler/gecko/nsProfilerStartParams.cpp
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "nsProfilerStartParams.h"
+#include "ipc/IPCMessageUtils.h"
+
+NS_IMPL_ISUPPORTS(nsProfilerStartParams, nsIProfilerStartParams)
+
+nsProfilerStartParams::nsProfilerStartParams(
+ uint32_t aEntries, const mozilla::Maybe<double>& aDuration,
+ double aInterval, uint32_t aFeatures, nsTArray<nsCString>&& aFilters,
+ uint64_t aActiveTabID)
+ : mEntries(aEntries),
+ mDuration(aDuration),
+ mInterval(aInterval),
+ mFeatures(aFeatures),
+ mFilters(std::move(aFilters)),
+ mActiveTabID(aActiveTabID) {}
+
+nsProfilerStartParams::~nsProfilerStartParams() {}
+
+NS_IMETHODIMP
+nsProfilerStartParams::GetEntries(uint32_t* aEntries) {
+ NS_ENSURE_ARG_POINTER(aEntries);
+ *aEntries = mEntries;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfilerStartParams::GetDuration(double* aDuration) {
+ NS_ENSURE_ARG_POINTER(aDuration);
+ if (mDuration) {
+ *aDuration = *mDuration;
+ } else {
+ *aDuration = 0;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfilerStartParams::GetInterval(double* aInterval) {
+ NS_ENSURE_ARG_POINTER(aInterval);
+ *aInterval = mInterval;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsProfilerStartParams::GetFeatures(uint32_t* aFeatures) {
+ NS_ENSURE_ARG_POINTER(aFeatures);
+ *aFeatures = mFeatures;
+ return NS_OK;
+}
+
+const nsTArray<nsCString>& nsProfilerStartParams::GetFilters() {
+ return mFilters;
+}
+
+NS_IMETHODIMP
+nsProfilerStartParams::GetActiveTabID(uint64_t* aActiveTabID) {
+ NS_ENSURE_ARG_POINTER(aActiveTabID);
+ *aActiveTabID = mActiveTabID;
+ return NS_OK;
+}
diff --git a/tools/profiler/gecko/nsProfilerStartParams.h b/tools/profiler/gecko/nsProfilerStartParams.h
new file mode 100644
index 0000000000..25c2b5082f
--- /dev/null
+++ b/tools/profiler/gecko/nsProfilerStartParams.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef _NSPROFILERSTARTPARAMS_H_
+#define _NSPROFILERSTARTPARAMS_H_
+
+#include "nsIProfiler.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+class nsProfilerStartParams : public nsIProfilerStartParams {
+ public:
+ // This class can be used on multiple threads. For example, it's used for the
+ // observer notification from profiler_start, which can run on any thread but
+ // posts the notification to the main thread.
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIPROFILERSTARTPARAMS
+
+ nsProfilerStartParams(uint32_t aEntries,
+ const mozilla::Maybe<double>& aDuration,
+ double aInterval, uint32_t aFeatures,
+ nsTArray<nsCString>&& aFilters, uint64_t aActiveTabID);
+
+ private:
+ virtual ~nsProfilerStartParams();
+ uint32_t mEntries;
+ mozilla::Maybe<double> mDuration;
+ double mInterval;
+ uint32_t mFeatures;
+ nsTArray<nsCString> mFilters;
+ uint64_t mActiveTabID;
+};
+
+#endif