diff options
Diffstat (limited to '')
-rw-r--r-- | tools/profiler/gecko/ChildProfilerController.cpp | 100 | ||||
-rw-r--r-- | tools/profiler/gecko/PProfiler.ipdl | 37 | ||||
-rw-r--r-- | tools/profiler/gecko/ProfilerChild.cpp | 295 | ||||
-rw-r--r-- | tools/profiler/gecko/ProfilerIOInterposeObserver.cpp | 200 | ||||
-rw-r--r-- | tools/profiler/gecko/ProfilerIOInterposeObserver.h | 29 | ||||
-rw-r--r-- | tools/profiler/gecko/ProfilerParent.cpp | 795 | ||||
-rw-r--r-- | tools/profiler/gecko/ProfilerTypes.ipdlh | 32 | ||||
-rw-r--r-- | tools/profiler/gecko/components.conf | 17 | ||||
-rw-r--r-- | tools/profiler/gecko/nsIProfiler.idl | 184 | ||||
-rw-r--r-- | tools/profiler/gecko/nsProfiler.cpp | 1038 | ||||
-rw-r--r-- | tools/profiler/gecko/nsProfiler.h | 72 | ||||
-rw-r--r-- | tools/profiler/gecko/nsProfilerCIID.h | 16 | ||||
-rw-r--r-- | tools/profiler/gecko/nsProfilerStartParams.cpp | 66 | ||||
-rw-r--r-- | tools/profiler/gecko/nsProfilerStartParams.h | 37 |
14 files changed, 2918 insertions, 0 deletions
diff --git a/tools/profiler/gecko/ChildProfilerController.cpp b/tools/profiler/gecko/ChildProfilerController.cpp new file mode 100644 index 0000000000..d3915b3e0b --- /dev/null +++ b/tools/profiler/gecko/ChildProfilerController.cpp @@ -0,0 +1,100 @@ +/* -*- 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/ipc/Endpoint.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() { + MOZ_COUNT_CTOR(ChildProfilerController); +} + +void ChildProfilerController::Init(Endpoint<PProfilerChild>&& aEndpoint) { + if (NS_SUCCEEDED( + NS_NewNamedThread("ProfilerChild", getter_AddRefs(mThread)))) { + // Now that mThread has been set, run SetupProfilerChild on the thread. + mThread->Dispatch( + NewRunnableMethod<Endpoint<PProfilerChild>&&>( + "ChildProfilerController::SetupProfilerChild", this, + &ChildProfilerController::SetupProfilerChild, std::move(aEndpoint)), + NS_DISPATCH_NORMAL); + } +} + +nsCString ChildProfilerController::GrabShutdownProfileAndShutdown() { + nsCString shutdownProfile; + ShutdownAndMaybeGrabShutdownProfileFirst(&shutdownProfile); + return shutdownProfile; +} + +void ChildProfilerController::Shutdown() { + ShutdownAndMaybeGrabShutdownProfileFirst(nullptr); +} + +void ChildProfilerController::ShutdownAndMaybeGrabShutdownProfileFirst( + nsCString* aOutShutdownProfile) { + if (mThread) { + mThread->Dispatch(NewRunnableMethod<nsCString*>( + "ChildProfilerController::ShutdownProfilerChild", + this, &ChildProfilerController::ShutdownProfilerChild, + aOutShutdownProfile), + NS_DISPATCH_NORMAL); + // Shut down the thread. This call will spin until all runnables (including + // the ShutdownProfilerChild runnable) have been processed. + mThread->Shutdown(); + mThread = nullptr; + } +} + +ChildProfilerController::~ChildProfilerController() { + MOZ_COUNT_DTOR(ChildProfilerController); + + MOZ_ASSERT(!mThread, + "Please call Shutdown before destroying ChildProfilerController"); + MOZ_ASSERT(!mProfilerChild); +} + +void ChildProfilerController::SetupProfilerChild( + Endpoint<PProfilerChild>&& aEndpoint) { + MOZ_RELEASE_ASSERT(mThread == 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( + nsCString* aOutShutdownProfile) { + MOZ_RELEASE_ASSERT(mThread == NS_GetCurrentThread()); + + if (aOutShutdownProfile) { + *aOutShutdownProfile = mProfilerChild->GrabShutdownProfile(); + } + mProfilerChild->Destroy(); + mProfilerChild = nullptr; +} + +} // namespace mozilla diff --git a/tools/profiler/gecko/PProfiler.ipdl b/tools/profiler/gecko/PProfiler.ipdl new file mode 100644 index 0000000000..85d47bc921 --- /dev/null +++ b/tools/profiler/gecko/PProfiler.ipdl @@ -0,0 +1,37 @@ +/* -*- 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. +async protocol PProfiler +{ +child: + async Start(ProfilerInitParams params); + async EnsureStarted(ProfilerInitParams params); + async Stop(); + async Pause(); + async Resume(); + async PauseSampling(); + async ResumeSampling(); + + async AwaitNextChunkManagerUpdate() returns (ProfileBufferChunkManagerUpdate update); + async DestroyReleasedChunksAtOrBefore(TimeStamp timeStamp); + + async GatherProfile() returns (Shmem profile); + + async ClearAllPages(); +}; + +} // namespace mozilla + diff --git a/tools/profiler/gecko/ProfilerChild.cpp b/tools/profiler/gecko/ProfilerChild.cpp new file mode 100644 index 0000000000..748f88ccfa --- /dev/null +++ b/tools/profiler/gecko/ProfilerChild.cpp @@ -0,0 +1,295 @@ +/* -*- 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 "ProfilerParent.h" + +#include "nsThreadUtils.h" + +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) { + 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.activeBrowsingContextID(), + params.duration()); + + SetupChunkManager(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult ProfilerChild::RecvEnsureStarted( + const ProfilerInitParams& params) { + 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.activeBrowsingContextID(), params.duration()); + + SetupChunkManager(); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult ProfilerChild::RecvStop() { + ResetChunkManager(); + profiler_stop(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult ProfilerChild::RecvPause() { + profiler_pause(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult ProfilerChild::RecvResume() { + profiler_resume(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult ProfilerChild::RecvPauseSampling() { + profiler_pause_sampling(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult ProfilerChild::RecvResumeSampling() { + profiler_resume_sampling(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult ProfilerChild::RecvClearAllPages() { + profiler_clear_all_pages(); + return IPC_OK(); +} + +static nsCString CollectProfileOrEmptyString(bool aIsShuttingDown) { + nsCString profileCString; + UniquePtr<char[]> profile = + profiler_get_profile(/* aSinceTime */ 0, aIsShuttingDown); + if (profile) { + size_t len = strlen(profile.get()); + profileCString.Adopt(profile.release(), len); + } + return profileCString; +} + +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(); +} + +mozilla::ipc::IPCResult ProfilerChild::RecvGatherProfile( + GatherProfileResolver&& aResolve) { + mozilla::ipc::Shmem shmem; + profiler_get_profile_json_into_lazily_allocated_buffer( + [&](size_t allocationSize) -> char* { + if (AllocShmem(allocationSize, + mozilla::ipc::Shmem::SharedMemory::TYPE_BASIC, &shmem)) { + return shmem.get<char>(); + } + return nullptr; + }, + /* aSinceTime */ 0, + /* aIsShuttingDown */ false); + aResolve(std::move(shmem)); + return IPC_OK(); +} + +void ProfilerChild::ActorDestroy(ActorDestroyReason aActorDestroyReason) { + mDestroyed = true; +} + +void ProfilerChild::Destroy() { + ResetChunkManager(); + if (!mDestroyed) { + Close(); + } +} + +nsCString ProfilerChild::GrabShutdownProfile() { + return CollectProfileOrEmptyString(/* aIsShuttingDown */ true); +} + +} // namespace mozilla diff --git a/tools/profiler/gecko/ProfilerIOInterposeObserver.cpp b/tools/profiler/gecko/ProfilerIOInterposeObserver.cpp new file mode 100644 index 0000000000..e1007c39fa --- /dev/null +++ b/tools/profiler/gecko/ProfilerIOInterposeObserver.cpp @@ -0,0 +1,200 @@ +/* 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; + +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()) { + aWriter.IntProperty("threadId", aOperationThreadId.ThreadId()); + } + } + 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); + 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_can_accept_markers()) { + return; + } + + const bool doCaptureStack = !(features & ProfilerFeature::NoIOStacks); + 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() || + !(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::MaybeCapture(doCaptureStack)), + 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()) { + // 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 = + doCaptureStack ? profiler_capture_backtrace() : nullptr; + + // 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()), + doCaptureStack ? MarkerStack::Capture() : MarkerStack::NoStack(), + // 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..13a3c84562 --- /dev/null +++ b/tools/profiler/gecko/ProfilerIOInterposeObserver.h @@ -0,0 +1,29 @@ +/* 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: + virtual void Observe(Observation& aObservation) override; + + protected: + 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..e5cc324a91 --- /dev/null +++ b/tools/profiler/gecko/ProfilerParent.cpp @@ -0,0 +1,795 @@ +/* -*- 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" + +#include "nsProfiler.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; + +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: + 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(); + + 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: + static void EnsureInstance(); + + // 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; + + // Singleton instance, created when first needed, destroyed at Firefox + // shutdown. + static UniquePtr<ProfilerParentTracker> sInstance; +}; + +ProfileBufferGlobalController::ProfileBufferGlobalController( + size_t aMaximumBytes) + : mMaximumBytes(aMaximumBytes) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + // 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)) { + 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. + 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()); + + 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]; + 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); + } +} + +UniquePtr<ProfilerParentTracker> ProfilerParentTracker::sInstance; + +/* static */ +void ProfilerParentTracker::EnsureInstance() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + if (sInstance) { + return; + } + + sInstance = 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(&sInstance, ShutdownPhase::ShutdownThreads); +} + +/* static */ +void ProfilerParentTracker::StartTracking(ProfilerParent* aProfilerParent) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + EnsureInstance(); + + if (sInstance->mMaybeController.isNothing() && sInstance->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.) + sInstance->mMaybeController.emplace(size_t(sInstance->mEntries) * 8u); + } + + sInstance->mProfilerParents.AppendElement(aProfilerParent); +} + +/* static */ +void ProfilerParentTracker::StopTracking(ProfilerParent* aParent) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + if (!sInstance) { + return; + } + + sInstance->mProfilerParents.RemoveElement(aParent); +} + +/* static */ +void ProfilerParentTracker::ProfilerStarted(uint32_t aEntries) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + EnsureInstance(); + + sInstance->mEntries = aEntries; + + if (sInstance->mMaybeController.isNothing() && + !sInstance->mProfilerParents.IsEmpty()) { + // We are already tracking child processes, so it's a good time to start + // controlling the global memory usage of the profiler. + sInstance->mMaybeController.emplace(size_t(sInstance->mEntries) * 8u); + } +} + +/* static */ +void ProfilerParentTracker::ProfilerWillStopIfStarted() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + if (!sInstance) { + return; + } + + sInstance->mEntries = 0; + sInstance->mMaybeController = Nothing{}; +} + +template <typename FuncType> +/* static */ +void ProfilerParentTracker::Enumerate(FuncType&& aIterFunc) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + if (!sInstance) { + return; + } + + for (ProfilerParent* profilerParent : sInstance->mProfilerParents) { + if (!profilerParent->mDestroyed) { + aIterFunc(profilerParent); + } + } +} + +template <typename FuncType> +/* static */ +void ProfilerParentTracker::ForChild(base::ProcessId aChildPid, + FuncType&& aIterFunc) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + if (!sInstance) { + return; + } + + for (ProfilerParent* profilerParent : sInstance->mProfilerParents) { + if (profilerParent->mChildPid == aChildPid) { + if (!profilerParent->mDestroyed) { + std::forward<FuncType>(aIterFunc)(profilerParent); + } + return; + } + } +} + +/* static */ +void ProfilerParentTracker::ForwardChildChunkManagerUpdate( + base::ProcessId aProcessId, + ProfileBufferControlledChunkManager::Update&& aUpdate) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + if (!sInstance || sInstance->mMaybeController.isNothing()) { + return; + } + + MOZ_ASSERT(!aUpdate.IsNotUpdate(), + "No process should ever send a non-update"); + sInstance->mMaybeController->HandleChildChunkManagerUpdate( + aProcessId, std::move(aUpdate)); +} + +ProfilerParentTracker::ProfilerParentTracker() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + MOZ_COUNT_CTOR(ProfilerParentTracker); +} + +ProfilerParentTracker::~ProfilerParentTracker() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + 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(); + } + } +} + +/* static */ +Endpoint<PProfilerChild> ProfilerParent::CreateForProcess( + base::ProcessId aOtherPid) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + Endpoint<PProfilerParent> parent; + Endpoint<PProfilerChild> child; + nsresult rv = PProfiler::CreateEndpoints(base::GetCurrentProcId(), aOtherPid, + &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!"); + } + + // mSelfRef will be cleared in DeallocPProfilerParent. + actor->mSelfRef = actor; + actor->Init(); + + return child; +} + +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 activeBrowsingContextID; + profiler_get_start_params(&entries, &duration, &interval, &features, &filters, + &activeBrowsingContextID); + + if (entries != 0) { + ProfilerInitParams ipcParams; + ipcParams.enabled() = true; + ipcParams.entries() = entries; + ipcParams.duration() = duration; + ipcParams.interval() = interval; + ipcParams.features() = features; + ipcParams.activeBrowsingContextID() = activeBrowsingContextID; + + for (uint32_t i = 0; i < filters.length(); ++i) { + ipcParams.filters().AppendElement(filters[i]); + } + + Unused << SendEnsureStarted(ipcParams); + RequestChunkManagerUpdate(); + } else { + Unused << SendStop(); + } +} + +ProfilerParent::~ProfilerParent() { + MOZ_COUNT_DTOR(ProfilerParent); + + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + ProfilerParentTracker::StopTracking(this); +} + +/* static */ +nsTArray<RefPtr<ProfilerParent::SingleProcessProfilePromise>> +ProfilerParent::GatherProfiles() { + if (!NS_IsMainThread()) { + return nsTArray<RefPtr<ProfilerParent::SingleProcessProfilePromise>>(); + } + + nsTArray<RefPtr<SingleProcessProfilePromise>> results; + ProfilerParentTracker::Enumerate([&](ProfilerParent* profilerParent) { + results.AppendElement(profilerParent->SendGatherProfile()); + }); + return results; +} + +// 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)); + }); +} + +/* static */ +void ProfilerParent::ProfilerStarted(nsIProfilerStartParams* aParams) { + if (!NS_IsMainThread()) { + return; + } + + 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(); + aParams->GetActiveBrowsingContextID(&ipcParams.activeBrowsingContextID()); + + ProfilerParentTracker::ProfilerStarted(ipcParams.entries()); + ProfilerParentTracker::Enumerate([&](ProfilerParent* profilerParent) { + Unused << profilerParent->SendStart(ipcParams); + profilerParent->RequestChunkManagerUpdate(); + }); +} + +/* static */ +void ProfilerParent::ProfilerWillStopIfStarted() { + if (!NS_IsMainThread()) { + return; + } + + ProfilerParentTracker::ProfilerWillStopIfStarted(); +} + +/* static */ +void ProfilerParent::ProfilerStopped() { + if (!NS_IsMainThread()) { + return; + } + + ProfilerParentTracker::Enumerate([](ProfilerParent* profilerParent) { + Unused << profilerParent->SendStop(); + }); +} + +/* static */ +void ProfilerParent::ProfilerPaused() { + if (!NS_IsMainThread()) { + return; + } + + ProfilerParentTracker::Enumerate([](ProfilerParent* profilerParent) { + Unused << profilerParent->SendPause(); + }); +} + +/* static */ +void ProfilerParent::ProfilerResumed() { + if (!NS_IsMainThread()) { + return; + } + + ProfilerParentTracker::Enumerate([](ProfilerParent* profilerParent) { + Unused << profilerParent->SendResume(); + }); +} + +/* static */ +void ProfilerParent::ProfilerPausedSampling() { + if (!NS_IsMainThread()) { + return; + } + + ProfilerParentTracker::Enumerate([](ProfilerParent* profilerParent) { + Unused << profilerParent->SendPauseSampling(); + }); +} + +/* static */ +void ProfilerParent::ProfilerResumedSampling() { + if (!NS_IsMainThread()) { + return; + } + + ProfilerParentTracker::Enumerate([](ProfilerParent* profilerParent) { + Unused << profilerParent->SendResumeSampling(); + }); +} + +/* static */ +void ProfilerParent::ClearAllPages() { + if (!NS_IsMainThread()) { + return; + } + + ProfilerParentTracker::Enumerate([](ProfilerParent* profilerParent) { + Unused << profilerParent->SendClearAllPages(); + }); +} + +void ProfilerParent::ActorDestroy(ActorDestroyReason aActorDestroyReason) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + mDestroyed = true; +} + +void ProfilerParent::ActorDealloc() { mSelfRef = nullptr; } + +} // namespace mozilla diff --git a/tools/profiler/gecko/ProfilerTypes.ipdlh b/tools/profiler/gecko/ProfilerTypes.ipdlh new file mode 100644 index 0000000000..b78225b6c4 --- /dev/null +++ b/tools/profiler/gecko/ProfilerTypes.ipdlh @@ -0,0 +1,32 @@ +/* -*- 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"; + +namespace mozilla { + +struct ProfilerInitParams { + bool enabled; + uint32_t entries; + double? duration; + double interval; + uint32_t features; + uint64_t activeBrowsingContextID; + nsCString[] filters; +}; + +struct ProfileBufferChunkMetadata { + TimeStamp doneTimeStamp; + uint32_t bufferBytes; +}; + +struct ProfileBufferChunkManagerUpdate { + uint64_t unreleasedBytes; + uint64_t releasedBytes; + TimeStamp oldestDoneTimeStamp; + ProfileBufferChunkMetadata[] newlyReleasedChunks; +}; + +} // 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..d767189c13 --- /dev/null +++ b/tools/profiler/gecko/nsIProfiler.idl @@ -0,0 +1,184 @@ +/* -*- 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" +%} + +[ref] native nsCString(const nsCString); +[ref] native StringArrayRef(const nsTArray<nsCString>); + +/** + * 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 activeBrowsingContextID; + + [noscript, notxpcom, nostdcall] StringArrayRef getFilters(); +}; + +[scriptable, builtinclass, uuid(ead3f75c-0e0e-4fbb-901c-1e5392ef5b2a)] +interface nsIProfiler : nsISupports +{ + boolean CanProfile(); + void StartProfiler(in uint32_t aEntries, in double aInterval, + in Array<AUTF8String> aFeatures, + [optional] in Array<AUTF8String> aFilters, + [optional] in uint64_t aActiveBrowsingContextID, + [optional] in double aDuration); + void StopProfiler(); + boolean IsPaused(); + void Pause(); + void Resume(); + boolean IsSamplingPaused(); + void PauseSampling(); + void ResumeSampling(); + + /* + * Resolves the returned promise after at least one full periodic sampling. + * Rejects the promise if sampler is not running (yet, or anymore, or paused). + * This is mainly useful in tests, to wait just long enough to guarantee that + * one sample was taken in the main 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); + + /** + * Returns a promise that resolves once the file has been written. + */ + [implicit_jscontext] + Promise dumpProfileToFileAsync(in ACString aFilename, + [optional] in double aSinceTime); + + 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); + + /** + * Dump the collected profile to a file. + */ + void dumpProfileToFile(in string aFilename); + + [noscript, notxpcom, nostdcall] + void receiveShutdownProfile(in nsCString aProfile); +}; diff --git a/tools/profiler/gecko/nsProfiler.cpp b/tools/profiler/gecko/nsProfiler.cpp new file mode 100644 index 0000000000..1edad8ea90 --- /dev/null +++ b/tools/profiler/gecko/nsProfiler.cpp @@ -0,0 +1,1038 @@ +/* -*- 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 <sstream> +#include <string> +#include <utility> + +#include "GeckoProfiler.h" +#include "ProfilerParent.h" +#include "js/Array.h" // JS::NewArrayObject +#include "js/JSON.h" +#include "js/Value.h" +#include "mozilla/ErrorResult.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 "nsIFileStreams.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsILoadContext.h" +#include "nsIObserverService.h" +#include "nsIWebNavigation.h" +#include "nsLocalFile.h" +#include "nsMemory.h" +#include "nsProfilerStartParams.h" +#include "nsProxyRelease.h" +#include "nsString.h" +#include "nsThreadUtils.h" +#include "platform.h" +#include "shared-libraries.h" +#include "zlib.h" + +using namespace mozilla; + +using dom::AutoJSAPI; +using dom::Promise; +using std::string; + +NS_IMPL_ISUPPORTS(nsProfiler, nsIProfiler, nsIObserver) + +nsProfiler::nsProfiler() + : mLockedForPrivateBrowsing(false), + mPendingProfiles(0), + mGathering(false) {} + +nsProfiler::~nsProfiler() { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->RemoveObserver(this, "chrome-document-global-created"); + observerService->RemoveObserver(this, "last-pb-context-exited"); + } + if (mSymbolTableThread) { + mSymbolTableThread->Shutdown(); + } + ResetGathering(); +} + +nsresult nsProfiler::Init() { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->AddObserver(this, "chrome-document-global-created", false); + observerService->AddObserver(this, "last-pb-context-exited", false); + } + return NS_OK; +} + +NS_IMETHODIMP +nsProfiler::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + // The profiler's handling of private browsing is as simple as possible: it + // is stopped when the first PB window opens, and left stopped when the last + // PB window closes. + if (strcmp(aTopic, "chrome-document-global-created") == 0) { + nsCOMPtr<nsIInterfaceRequestor> requestor = do_QueryInterface(aSubject); + nsCOMPtr<nsIWebNavigation> parentWebNav = do_GetInterface(requestor); + nsCOMPtr<nsILoadContext> loadContext = do_QueryInterface(parentWebNav); + if (loadContext && loadContext->UsePrivateBrowsing() && + !mLockedForPrivateBrowsing) { + mLockedForPrivateBrowsing = true; + // Allow profiling tests that trigger private browsing. + if (!xpc::IsInAutomation()) { + profiler_stop(); + } + } + } else if (strcmp(aTopic, "last-pb-context-exited") == 0) { + mLockedForPrivateBrowsing = false; + } + return NS_OK; +} + +NS_IMETHODIMP +nsProfiler::CanProfile(bool* aCanProfile) { + *aCanProfile = !mLockedForPrivateBrowsing; + return NS_OK; +} + +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; +} + +NS_IMETHODIMP +nsProfiler::StartProfiler(uint32_t aEntries, double aInterval, + const nsTArray<nsCString>& aFeatures, + const nsTArray<nsCString>& aFilters, + uint64_t aActiveBrowsingContextID, double aDuration) { + if (mLockedForPrivateBrowsing) { + return NS_ERROR_NOT_AVAILABLE; + } + + ResetGathering(); + + 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; + } + profiler_start(PowerOfTwo32(aEntries), aInterval, features, + filterStringVector.begin(), filterStringVector.length(), + aActiveBrowsingContextID, duration); + + return NS_OK; +} + +NS_IMETHODIMP +nsProfiler::StopProfiler() { + ResetGathering(); + + profiler_stop(); + + return NS_OK; +} + +NS_IMETHODIMP +nsProfiler::IsPaused(bool* aIsPaused) { + *aIsPaused = profiler_is_paused(); + return NS_OK; +} + +NS_IMETHODIMP +nsProfiler::Pause() { + profiler_pause(); + return NS_OK; +} + +NS_IMETHODIMP +nsProfiler::Resume() { + profiler_resume(); + return NS_OK; +} + +NS_IMETHODIMP +nsProfiler::IsSamplingPaused(bool* aIsSamplingPaused) { + *aIsSamplingPaused = profiler_is_sampling_paused(); + return NS_OK; +} + +NS_IMETHODIMP +nsProfiler::PauseSampling() { + profiler_pause_sampling(); + return NS_OK; +} + +NS_IMETHODIMP +nsProfiler::ResumeSampling() { + profiler_resume_sampling(); + return NS_OK; +} + +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]() { + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init( + promiseHandleInMT->GetGlobalObject()))) { + // We're really hosed if we can't get a JS context for + // some reason. + promiseHandleInMT->MaybeReject( + NS_ERROR_DOM_UNKNOWN_ERR); + return; + } + + switch (aSamplingState) { + case SamplingState::JustStopped: + case SamplingState::SamplingPaused: + promiseHandleInMT->MaybeReject(NS_ERROR_FAILURE); + break; + + case SamplingState::NoStackSamplingCompleted: + case SamplingState::SamplingCompleted: { + JS::RootedValue val(jsapi.cx()); + promiseHandleInMT->MaybeResolve(val); + } 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; +} + +namespace { +struct StringWriteFunc : public JSONWriteFunc { + nsAString& mBuffer; // This struct must not outlive this buffer + explicit StringWriteFunc(nsAString& buffer) : mBuffer(buffer) {} + + void Write(const Span<const char>& aStr) override { + mBuffer.Append(NS_ConvertUTF8toUTF16(aStr.data(), aStr.size())); + } +}; +} // namespace + +NS_IMETHODIMP +nsProfiler::GetSharedLibraries(JSContext* aCx, + JS::MutableHandle<JS::Value> aResult) { + JS::RootedValue val(aCx); + { + nsString buffer; + JSONWriter w(MakeUnique<StringWriteFunc>(buffer)); + w.StartArrayElement(); + AppendSharedLibraries(w); + w.EndArray(); + MOZ_ALWAYS_TRUE(JS_ParseJSON(aCx, + static_cast<const char16_t*>(buffer.get()), + buffer.Length(), &val)); + } + JS::RootedObject 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::RootedValue jsValue(aCx); + { + nsString buffer; + JSONWriter writer(MakeUnique<StringWriteFunc>(buffer)); + profiler_write_active_configuration(writer); + MOZ_ALWAYS_TRUE(JS_ParseJSON(aCx, + static_cast<const char16_t*>(buffer.get()), + buffer.Length(), &jsValue)); + } + if (jsValue.isNull()) { + aResult.setNull(); + } else { + JS::RootedObject 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::RootedValue 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](nsCString 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::RootedValue val(cx); + { + NS_ConvertUTF8toUTF16 js_string(aResult); + 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::RootedValue 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](nsCString 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.Length(), + reinterpret_cast<const uint8_t*>(aResult.Data())); + if (typedArray) { + JS::RootedValue 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; +} + +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](nsCString 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; + } + + // 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(aResult.Length()); + FallibleTArray<uint8_t> outBuff; + if (!outBuff.SetLength(outSize, fallible)) { + promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY); + return; + } + + int zerr; + z_stream stream; + stream.zalloc = nullptr; + stream.zfree = nullptr; + stream.opaque = nullptr; + stream.next_out = (Bytef*)outBuff.Elements(); + stream.avail_out = outBuff.Length(); + stream.next_in = (z_const Bytef*)aResult.Data(); + stream.avail_in = aResult.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) { + promise->MaybeReject(NS_ERROR_FAILURE); + return; + } + + zerr = deflate(&stream, Z_FINISH); + outSize = stream.total_out; + deflateEnd(&stream); + + if (zerr != Z_STREAM_END) { + promise->MaybeReject(NS_ERROR_FAILURE); + return; + } + + outBuff.TruncateLength(outSize); + + JSContext* cx = jsapi.cx(); + JSObject* typedArray = dom::ArrayBuffer::Create( + cx, outBuff.Length(), outBuff.Elements()); + if (typedArray) { + JS::RootedValue 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; +} + +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 nsCString& aResult) { + nsCOMPtr<nsIFile> file = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID); + nsresult rv = file->InitWithNativePath(filename); + if (NS_FAILED(rv)) { + MOZ_CRASH(); + } + nsCOMPtr<nsIFileOutputStream> of = + do_CreateInstance("@mozilla.org/network/file-output-stream;1"); + of->Init(file, -1, -1, 0); + uint32_t sz; + of->Write(aResult.get(), aResult.Length(), &sz); + of->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::RootedObject addrsArray( + cx, dom::Uint32Array::Create(cx, aSymbolTable.mAddrs.Length(), + aSymbolTable.mAddrs.Elements())); + JS::RootedObject indexArray( + cx, dom::Uint32Array::Create(cx, aSymbolTable.mIndex.Length(), + aSymbolTable.mIndex.Elements())); + JS::RootedObject bufferArray( + cx, dom::Uint8Array::Create(cx, aSymbolTable.mBuffer.Length(), + aSymbolTable.mBuffer.Elements())); + + if (addrsArray && indexArray && bufferArray) { + JS::RootedObject 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; +} + +/* 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; + } + self->mGatheringTimer = nullptr; + if (!profiler_is_active() || !self->mGathering) { + // Not gathering anymore. + return; + } + 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::GatheredOOPProfile(const nsACString& aProfile) { + 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"); + + if (!aProfile.IsEmpty()) { + // TODO: Remove PromiseFlatCString, see bug 1657033. + mWriter->Splice(PromiseFlatCString(aProfile)); + } + + mPendingProfiles--; + + if (mPendingProfiles == 0) { + // 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. + if (mGatheringTimer) { + uint32_t delayMs = 0; + const nsresult r = mGatheringTimer->GetDelay(&delayMs); + mGatheringTimer->Cancel(); + mGatheringTimer = nullptr; + if (NS_SUCCEEDED(r) && delayMs != 0) { + Unused << NS_NewTimerWithFuncCallback( + getter_AddRefs(mGatheringTimer), GatheringTimerCallback, this, + delayMs, nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY, "", + GetMainThreadSerialEventTarget()); + } + } +} + +void nsProfiler::ReceiveShutdownProfile(const nsCString& aProfile) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + profiler_received_exit_profile(aProfile); +} + +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; + + if (mGatheringTimer) { + mGatheringTimer->Cancel(); + mGatheringTimer = nullptr; + } + + // 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<RefPtr<ProfilerParent::SingleProcessProfilePromise>> profiles = + ProfilerParent::GatherProfiles(); + + mWriter.emplace(); + + TimeStamp streamingStart = TimeStamp::NowUnfuzzed(); + + UniquePtr<ProfilerCodeAddressService> service = + profiler_code_address_service_for_presymbolication(); + + // Start building up the JSON result and grab the profile from this process. + mWriter->Start(); + if (!profiler_stream_json_for_this_process(*mWriter, aSinceTime, + /* aIsShuttingDown */ false, + service.get())) { + // 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. + return GatheringPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); + } + + mWriter->StartArrayProperty("processes"); + + // If we have any process exit profiles, add them immediately. + Vector<nsCString> exitProfiles = profiler_move_exit_profiles(); + for (auto& exitProfile : exitProfiles) { + if (!exitProfile.IsEmpty()) { + mWriter->Splice(exitProfile); + } + } + + 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. + + mPendingProfiles = profiles.Length(); + if (mPendingProfiles != 0) { + // There *are* pending profiles, let's add handlers for their promises. + + // We want a reasonable timeout value while waiting for child profiles. + // We know how long the parent process took to serialize its profile: + const uint32_t parentTimeMs = static_cast<uint32_t>( + (TimeStamp::NowUnfuzzed() - streamingStart).ToMilliseconds()); + // We will multiply this by the number of children, to cover the worst case + // where all processes take the same time, but because they are working in + // parallel on a potential single CPU, they all finish around the same later + // time. + // And multiply again by 2, for the extra processing and comms, and other + // work that may happen. + const uint32_t parentToChildrenFactor = mPendingProfiles * 2; + // And we add a number seconds by default. In some lopsided cases, the + // parent-to-child serializing ratio could be much greater than expected, + // so the user could force it to be a bigger number if needed. + uint32_t childTimeoutS = Preferences::GetUint( + "devtools.performance.recording.child.timeout_s", 0u); + if (childTimeoutS == 0) { + // If absent or 0, use hard-coded default. + childTimeoutS = 1; + } + // And this gives us a timeout value. The timer will be restarted after we + // receive each response. + // TODO: Instead of a timeout to cover the whole request-to-response time, + // there should be more of a continuous dialog between processes, to only + // give up if some processes are really unresponsive. See bug 1673513. + const uint32_t streamingTimeoutMs = + parentTimeMs * parentToChildrenFactor + childTimeoutS * 1000; + Unused << NS_NewTimerWithFuncCallback( + getter_AddRefs(mGatheringTimer), GatheringTimerCallback, this, + streamingTimeoutMs, nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY, "", + GetMainThreadSerialEventTarget()); + + for (auto profile : profiles) { + profile->Then( + GetMainThreadSerialEventTarget(), __func__, + [self = RefPtr<nsProfiler>(this)](mozilla::ipc::Shmem&& aResult) { + const nsDependentCSubstring profileString(aResult.get<char>(), + aResult.Size<char>() - 1); + self->GatheredOOPProfile(profileString); + }, + [self = + RefPtr<nsProfiler>(this)](ipc::ResponseRejectReason&& aReason) { + self->GatheredOOPProfile(""_ns); + }); + } + } else { + // There are no pending profiles, we're already done. + 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()); + + // Close the "processes" array property. + mWriter->EndArray(); + + // Close the root object of the generated JSON. + mWriter->End(); + + UniquePtr<char[]> buf = mWriter->ChunkedWriteFunc().CopyData(); + size_t len = strlen(buf.get()); + nsCString result; + result.Adopt(buf.release(), len); + mPromiseHolder->Resolve(std::move(result), __func__); + + ResetGathering(); +} + +void nsProfiler::ResetGathering() { + // If we have an unfulfilled Promise in flight, we should reject it before + // destroying the promise holder. + if (mPromiseHolder.isSome()) { + mPromiseHolder->RejectIfExists(NS_ERROR_DOM_ABORT_ERR, __func__); + mPromiseHolder.reset(); + } + mPendingProfiles = 0; + mGathering = false; + if (mGatheringTimer) { + mGatheringTimer->Cancel(); + mGatheringTimer = nullptr; + } + mWriter.reset(); +} diff --git a/tools/profiler/gecko/nsProfiler.h b/tools/profiler/gecko/nsProfiler.h new file mode 100644 index 0000000000..1f05cab515 --- /dev/null +++ b/tools/profiler/gecko/nsProfiler.h @@ -0,0 +1,72 @@ +/* -*- 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 "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "mozilla/ProfileJSONWriter.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/Vector.h" +#include "nsIObserver.h" +#include "nsIProfiler.h" +#include "nsITimer.h" +#include "nsServiceManagerUtils.h" +#include "ProfilerCodeAddressService.h" + +class nsProfiler final : public nsIProfiler, public nsIObserver { + public: + nsProfiler(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIPROFILER + + nsresult Init(); + + static nsProfiler* GetOrCreate() { + nsCOMPtr<nsIProfiler> iprofiler = + do_GetService("@mozilla.org/tools/profiler;1"); + return static_cast<nsProfiler*>(iprofiler.get()); + } + + void GatheredOOPProfile(const nsACString& aProfile); + + private: + ~nsProfiler(); + + typedef mozilla::MozPromise<nsCString, nsresult, false> GatheringPromise; + typedef mozilla::MozPromise<mozilla::SymbolTable, nsresult, true> + SymbolTablePromise; + + RefPtr<GatheringPromise> StartGathering(double aSinceTime); + void FinishGathering(); + void ResetGathering(); + static void GatheringTimerCallback(nsITimer* aTimer, void* aClosure); + + RefPtr<SymbolTablePromise> GetSymbolTableMozPromise( + const nsACString& aDebugPath, const nsACString& aBreakpadID); + + bool mLockedForPrivateBrowsing; + + struct ExitProfile { + nsCString mJSON; + uint64_t mBufferPositionAtGatherTime; + }; + + // These fields are all related to profile gathering. + mozilla::Vector<ExitProfile> mExitProfiles; + mozilla::Maybe<mozilla::MozPromiseHolder<GatheringPromise>> mPromiseHolder; + nsCOMPtr<nsIThread> mSymbolTableThread; + mozilla::Maybe<SpliceableChunkedJSONWriter> mWriter; + uint32_t mPendingProfiles; + bool mGathering; + nsCOMPtr<nsITimer> mGatheringTimer; +}; + +#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..dfe1102668 --- /dev/null +++ b/tools/profiler/gecko/nsProfilerStartParams.cpp @@ -0,0 +1,66 @@ +/* -*- 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 aActiveBrowsingContextID) + : mEntries(aEntries), + mDuration(aDuration), + mInterval(aInterval), + mFeatures(aFeatures), + mFilters(std::move(aFilters)), + mActiveBrowsingContextID(aActiveBrowsingContextID) {} + +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::GetActiveBrowsingContextID( + uint64_t* aActiveBrowsingContextID) { + NS_ENSURE_ARG_POINTER(aActiveBrowsingContextID); + *aActiveBrowsingContextID = mActiveBrowsingContextID; + return NS_OK; +} diff --git a/tools/profiler/gecko/nsProfilerStartParams.h b/tools/profiler/gecko/nsProfilerStartParams.h new file mode 100644 index 0000000000..c06439c357 --- /dev/null +++ b/tools/profiler/gecko/nsProfilerStartParams.h @@ -0,0 +1,37 @@ +/* -*- 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 aActiveBrowsingContextID); + + private: + virtual ~nsProfilerStartParams(); + uint32_t mEntries; + mozilla::Maybe<double> mDuration; + double mInterval; + uint32_t mFeatures; + nsTArray<nsCString> mFilters; + uint64_t mActiveBrowsingContextID; +}; + +#endif |