diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /tools/profiler/gecko | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | tools/profiler/gecko/ChildProfilerController.cpp | 170 | ||||
-rw-r--r-- | tools/profiler/gecko/PProfiler.ipdl | 44 | ||||
-rw-r--r-- | tools/profiler/gecko/ProfilerChild.cpp | 565 | ||||
-rw-r--r-- | tools/profiler/gecko/ProfilerIOInterposeObserver.cpp | 216 | ||||
-rw-r--r-- | tools/profiler/gecko/ProfilerIOInterposeObserver.h | 32 | ||||
-rw-r--r-- | tools/profiler/gecko/ProfilerParent.cpp | 1002 | ||||
-rw-r--r-- | tools/profiler/gecko/ProfilerTypes.ipdlh | 43 | ||||
-rw-r--r-- | tools/profiler/gecko/components.conf | 17 | ||||
-rw-r--r-- | tools/profiler/gecko/nsIProfiler.idl | 208 | ||||
-rw-r--r-- | tools/profiler/gecko/nsProfiler.cpp | 1487 | ||||
-rw-r--r-- | tools/profiler/gecko/nsProfiler.h | 117 | ||||
-rw-r--r-- | tools/profiler/gecko/nsProfilerCIID.h | 16 | ||||
-rw-r--r-- | tools/profiler/gecko/nsProfilerStartParams.cpp | 65 | ||||
-rw-r--r-- | tools/profiler/gecko/nsProfilerStartParams.h | 36 |
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 |