summaryrefslogtreecommitdiffstats
path: root/tools/profiler/gecko
diff options
context:
space:
mode:
Diffstat (limited to 'tools/profiler/gecko')
-rw-r--r--tools/profiler/gecko/ChildProfilerController.cpp100
-rw-r--r--tools/profiler/gecko/PProfiler.ipdl37
-rw-r--r--tools/profiler/gecko/ProfilerChild.cpp295
-rw-r--r--tools/profiler/gecko/ProfilerIOInterposeObserver.cpp200
-rw-r--r--tools/profiler/gecko/ProfilerIOInterposeObserver.h29
-rw-r--r--tools/profiler/gecko/ProfilerParent.cpp795
-rw-r--r--tools/profiler/gecko/ProfilerTypes.ipdlh32
-rw-r--r--tools/profiler/gecko/components.conf17
-rw-r--r--tools/profiler/gecko/nsIProfiler.idl184
-rw-r--r--tools/profiler/gecko/nsProfiler.cpp1038
-rw-r--r--tools/profiler/gecko/nsProfiler.h72
-rw-r--r--tools/profiler/gecko/nsProfilerCIID.h16
-rw-r--r--tools/profiler/gecko/nsProfilerStartParams.cpp66
-rw-r--r--tools/profiler/gecko/nsProfilerStartParams.h37
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