summaryrefslogtreecommitdiffstats
path: root/toolkit/components/backgroundhangmonitor
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/backgroundhangmonitor
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/backgroundhangmonitor')
-rw-r--r--toolkit/components/backgroundhangmonitor/BHRTelemetryService.sys.mjs161
-rw-r--r--toolkit/components/backgroundhangmonitor/BackgroundHangMonitor.cpp766
-rw-r--r--toolkit/components/backgroundhangmonitor/BackgroundHangMonitor.h198
-rw-r--r--toolkit/components/backgroundhangmonitor/HangAnnotations.cpp89
-rw-r--r--toolkit/components/backgroundhangmonitor/HangAnnotations.h71
-rw-r--r--toolkit/components/backgroundhangmonitor/HangDetails.cpp737
-rw-r--r--toolkit/components/backgroundhangmonitor/HangDetails.h101
-rw-r--r--toolkit/components/backgroundhangmonitor/HangTypes.ipdlh95
-rw-r--r--toolkit/components/backgroundhangmonitor/ThreadStackHelper.cpp395
-rw-r--r--toolkit/components/backgroundhangmonitor/ThreadStackHelper.h111
-rw-r--r--toolkit/components/backgroundhangmonitor/components.conf16
-rw-r--r--toolkit/components/backgroundhangmonitor/moz.build68
-rw-r--r--toolkit/components/backgroundhangmonitor/nsIHangDetails.idl77
-rw-r--r--toolkit/components/backgroundhangmonitor/tests/child_cause_hang.js23
-rw-r--r--toolkit/components/backgroundhangmonitor/tests/test_BHRObserver.js164
-rw-r--r--toolkit/components/backgroundhangmonitor/tests/xpcshell.toml14
16 files changed, 3086 insertions, 0 deletions
diff --git a/toolkit/components/backgroundhangmonitor/BHRTelemetryService.sys.mjs b/toolkit/components/backgroundhangmonitor/BHRTelemetryService.sys.mjs
new file mode 100644
index 0000000000..98c1274b5c
--- /dev/null
+++ b/toolkit/components/backgroundhangmonitor/BHRTelemetryService.sys.mjs
@@ -0,0 +1,161 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs",
+});
+
+export function BHRTelemetryService() {
+ // Allow tests to get access to this object to verify it works correctly.
+ this.wrappedJSObject = this;
+
+ Services.obs.addObserver(this, "profile-before-change");
+ Services.obs.addObserver(this, "bhr-thread-hang");
+ Services.obs.addObserver(this, "idle-daily");
+
+ this.resetPayload();
+}
+
+BHRTelemetryService.prototype = Object.freeze({
+ classID: Components.ID("{117c8cdf-69e6-4f31-a439-b8a654c67127}"),
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ TRANSMIT_HANG_COUNT: 50,
+
+ resetPayload() {
+ this.startTime = +new Date();
+ this.payload = {
+ modules: [],
+ hangs: [],
+ };
+ this.clearPermahangFile = false;
+ },
+
+ recordHang({
+ duration,
+ thread,
+ runnableName,
+ process,
+ stack,
+ remoteType,
+ modules,
+ annotations,
+ wasPersisted,
+ }) {
+ if (!Services.telemetry.canRecordExtended) {
+ return;
+ }
+
+ // Create a mapping from module indicies in the original nsIHangDetails
+ // object to this.payload.modules indicies.
+ let moduleIdxs = modules.map(module => {
+ let idx = this.payload.modules.findIndex(m => {
+ return m[0] === module[0] && m[1] === module[1];
+ });
+ if (idx === -1) {
+ idx = this.payload.modules.length;
+ this.payload.modules.push(module);
+ }
+ return idx;
+ });
+
+ // Native stack frames are [modIdx, offset] arrays. If we have a valid
+ // module index, we want to map it to the this.payload.modules array.
+ for (let i = 0; i < stack.length; ++i) {
+ if (Array.isArray(stack[i]) && stack[i][0] !== -1) {
+ stack[i][0] = moduleIdxs[stack[i][0]];
+ } else if (typeof stack[i] == "string") {
+ // This is just a precaution - we don't currently know of sensitive
+ // URLs being included in label frames' dynamic strings which we
+ // include here, but this is just an added guard. Here we strip any
+ // string with a :// in it that isn't a chrome:// or resource://
+ // URL. This is not completely robust, but we are already trying to
+ // protect against this by only including dynamic strings from the
+ // opt-in AUTO_PROFILER_..._NONSENSITIVE macros.
+ let match = /[^\s]+:\/\/.*/.exec(stack[i]);
+ if (
+ match &&
+ !match[0].startsWith("chrome://") &&
+ !match[0].startsWith("resource://")
+ ) {
+ stack[i] = stack[i].replace(match[0], "(excluded)");
+ }
+ }
+ }
+
+ // Create the hang object to record in the payload.
+ this.payload.hangs.push({
+ duration,
+ thread,
+ runnableName,
+ process,
+ remoteType,
+ annotations,
+ stack,
+ });
+
+ if (wasPersisted) {
+ this.clearPermahangFile = true;
+ }
+
+ // If we have collected enough hangs, we can submit the hangs we have
+ // collected to telemetry.
+ if (this.payload.hangs.length > this.TRANSMIT_HANG_COUNT) {
+ this.submit();
+ }
+ },
+
+ submit() {
+ if (this.clearPermahangFile) {
+ // NB: This is async but it is called from an Observer callback.
+ IOUtils.remove(
+ PathUtils.join(PathUtils.profileDir, "last_permahang.bin")
+ );
+ }
+
+ if (!Services.telemetry.canRecordExtended) {
+ return;
+ }
+
+ // NOTE: We check a separate bhrPing.enabled pref here. This pref is unset
+ // when running tests so that we run as much of BHR as possible (to catch
+ // errors) while avoiding timeouts caused by invoking `pingsender` during
+ // testing.
+ if (
+ Services.prefs.getBoolPref("toolkit.telemetry.bhrPing.enabled", false)
+ ) {
+ this.payload.timeSinceLastPing = new Date() - this.startTime;
+ lazy.TelemetryController.submitExternalPing("bhr", this.payload, {
+ addEnvironment: true,
+ });
+ }
+ this.resetPayload();
+ },
+
+ shutdown() {
+ Services.obs.removeObserver(this, "profile-before-change");
+ Services.obs.removeObserver(this, "bhr-thread-hang");
+ Services.obs.removeObserver(this, "idle-daily");
+ this.submit();
+ },
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "profile-after-change":
+ this.resetPayload();
+ break;
+ case "bhr-thread-hang":
+ this.recordHang(aSubject.QueryInterface(Ci.nsIHangDetails));
+ break;
+ case "profile-before-change":
+ this.shutdown();
+ break;
+ case "idle-daily":
+ this.submit();
+ break;
+ }
+ },
+});
diff --git a/toolkit/components/backgroundhangmonitor/BackgroundHangMonitor.cpp b/toolkit/components/backgroundhangmonitor/BackgroundHangMonitor.cpp
new file mode 100644
index 0000000000..3989495ab3
--- /dev/null
+++ b/toolkit/components/backgroundhangmonitor/BackgroundHangMonitor.cpp
@@ -0,0 +1,766 @@
+/* -*- 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 "mozilla/BackgroundHangMonitor.h"
+
+#include <string_view>
+#include <utility>
+
+#include "GeckoProfiler.h"
+#include "HangDetails.h"
+#include "ThreadStackHelper.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/CPUUsageWatcher.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_toolkit.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/ThreadLocal.h"
+#include "mozilla/Unused.h"
+#if defined(XP_WIN)
+# include "mozilla/WindowsStackWalkInitialization.h"
+#endif // XP_WIN
+#include "mozilla/dom/RemoteType.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+#include "prinrval.h"
+#include "prthread.h"
+
+#include <algorithm>
+
+#if defined(XP_WIN)
+# include "mozilla/NativeNt.h"
+#endif
+
+// Activate BHR only for one every BHR_BETA_MOD users.
+// We're doing experimentation with collecting a lot more data from BHR, and
+// don't want to enable it for beta users at the moment. We can scale this up in
+// the future.
+#define BHR_BETA_MOD INT32_MAX;
+
+// Interval at which we check the global and per-process CPU usage in order to
+// determine if there is high external CPU usage.
+static const int32_t kCheckCPUIntervalMilliseconds = 2000;
+
+// An utility comparator function used by std::unique to collapse "(* script)"
+// entries in a vector representing a call stack.
+bool StackScriptEntriesCollapser(const char* aStackEntry,
+ const char* aAnotherStackEntry) {
+ return !strcmp(aStackEntry, aAnotherStackEntry) &&
+ (!strcmp(aStackEntry, "(chrome script)") ||
+ !strcmp(aStackEntry, "(content script)"));
+}
+
+namespace mozilla {
+
+/**
+ * BackgroundHangManager is the global object that
+ * manages all instances of BackgroundHangThread.
+ */
+class BackgroundHangManager : public nsIObserver {
+ private:
+ // Stop hang monitoring
+ bool mShutdown;
+
+ BackgroundHangManager(const BackgroundHangManager&);
+ BackgroundHangManager& operator=(const BackgroundHangManager&);
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ static StaticRefPtr<BackgroundHangManager> sInstance;
+ static bool sDisabled;
+
+ // Lock for access to members of this class
+ Monitor mLock MOZ_UNANNOTATED;
+ // List of BackgroundHangThread instances associated with each thread
+ LinkedList<BackgroundHangThread> mHangThreads;
+
+ // Unwinding and reporting of hangs is despatched to this thread.
+ nsCOMPtr<nsIThread> mHangProcessingThread;
+
+ // Hang monitor thread
+ nsCOMPtr<nsIThread> mHangMonitorThread;
+
+ ProfilerThreadId mHangMonitorProfilerThreadId;
+
+ void InitMonitorThread() {
+ mHangMonitorProfilerThreadId = profiler_current_thread_id();
+#if defined(MOZ_GECKO_PROFILER) && defined(XP_WIN) && defined(_M_X64)
+ // Pre-commit 5 more pages of stack to guarantee enough commited stack
+ // space on this thread upon hang detection, when we will need to run
+ // profiler_suspend_and_sample_thread (bug 1840164).
+ mozilla::nt::CheckStack(5 * 0x1000);
+#endif
+ }
+
+ // Used for recording a permahang in case we don't ever make it back to
+ // the main thread to record/send it.
+ nsCOMPtr<nsIFile> mPermahangFile;
+
+ // Allows us to watch CPU usage and annotate hangs when the system is
+ // under high external load.
+ CPUUsageWatcher mCPUUsageWatcher;
+
+ TimeStamp mLastCheckedCPUUsage;
+
+ void CollectCPUUsage(TimeStamp aNow, bool aForce = false) {
+ if (aForce ||
+ aNow - mLastCheckedCPUUsage >
+ TimeDuration::FromMilliseconds(kCheckCPUIntervalMilliseconds)) {
+ Unused << NS_WARN_IF(mCPUUsageWatcher.CollectCPUUsage().isErr());
+ mLastCheckedCPUUsage = aNow;
+ }
+ }
+
+ void Shutdown() {
+ MonitorAutoLock autoLock(mLock);
+ mShutdown = true;
+ }
+
+ BackgroundHangManager();
+
+ private:
+ virtual ~BackgroundHangManager();
+};
+
+NS_IMPL_ISUPPORTS(BackgroundHangManager, nsIObserver)
+
+NS_IMETHODIMP
+BackgroundHangManager::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!strcmp(aTopic, "browser-delayed-startup-finished")) {
+ MonitorAutoLock autoLock(mLock);
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(mPermahangFile));
+ if (NS_SUCCEEDED(rv)) {
+ mPermahangFile->AppendNative("last_permahang.bin"_ns);
+ } else {
+ mPermahangFile = nullptr;
+ }
+
+ if (mHangProcessingThread && mPermahangFile) {
+ nsCOMPtr<nsIRunnable> submitRunnable =
+ new SubmitPersistedPermahangRunnable(mPermahangFile);
+ mHangProcessingThread->Dispatch(submitRunnable.forget());
+ }
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(observerService);
+ observerService->RemoveObserver(BackgroundHangManager::sInstance,
+ "browser-delayed-startup-finished");
+ } else if (!strcmp(aTopic, "profile-after-change")) {
+ BackgroundHangMonitor::DisableOnBeta();
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(observerService);
+ observerService->RemoveObserver(BackgroundHangManager::sInstance,
+ "profile-after-change");
+ } else {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ return NS_OK;
+}
+
+/**
+ * BackgroundHangThread is a per-thread object that is used
+ * by all instances of BackgroundHangMonitor to monitor hangs.
+ */
+class BackgroundHangThread final
+ : public LinkedListElement<BackgroundHangThread>,
+ public nsITimerCallback,
+ public nsINamed {
+ private:
+ static MOZ_THREAD_LOCAL(BackgroundHangThread*) sTlsKey;
+ static bool sTlsKeyInitialized;
+
+ BackgroundHangThread(const BackgroundHangThread&);
+ BackgroundHangThread& operator=(const BackgroundHangThread&);
+ ~BackgroundHangThread();
+
+ /* Keep a reference to the manager, so we can keep going even
+ after BackgroundHangManager::Shutdown is called. */
+ const RefPtr<BackgroundHangManager> mManager;
+ // Unique thread ID for identification
+ const PRThread* mThreadID;
+ RefPtr<nsITimer> mTimer;
+ TimeStamp mExpectedTimerNotification;
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ /**
+ * Returns the BackgroundHangThread associated with the
+ * running thread. Note that this will not find private
+ * BackgroundHangThread threads.
+ *
+ * @return BackgroundHangThread*, or nullptr if no thread
+ * is found.
+ */
+ static BackgroundHangThread* FindThread();
+
+ static void Startup() {
+ /* We can tolerate init() failing. */
+ sTlsKeyInitialized = sTlsKey.init();
+ }
+
+ // Hang timeout
+ const TimeDuration mTimeout;
+ // PermaHang timeout
+ const TimeDuration mMaxTimeout;
+ // Time at last activity
+ TimeStamp mLastActivity;
+ // Time when a hang started
+ TimeStamp mHangStart;
+ // Is the thread in a hang
+ bool mHanging;
+ // Is the thread in a waiting state
+ bool mWaiting;
+ // Is the thread dedicated to a single BackgroundHangMonitor
+ BackgroundHangMonitor::ThreadType mThreadType;
+#ifdef MOZ_GECKO_PROFILER
+ // Platform-specific helper to get hang stacks
+ ThreadStackHelper mStackHelper;
+#endif
+ // Stack of current hang
+ HangStack mHangStack;
+ // Annotations for the current hang
+ BackgroundHangAnnotations mAnnotations;
+ // Annotators registered for this thread
+ BackgroundHangAnnotators mAnnotators;
+ // The name of the runnable which is hanging the current process
+ nsCString mRunnableName;
+ // The name of the thread which is being monitored
+ nsCString mThreadName;
+
+ BackgroundHangThread(const char* aName, uint32_t aTimeoutMs,
+ uint32_t aMaxTimeoutMs,
+ BackgroundHangMonitor::ThreadType aThreadType =
+ BackgroundHangMonitor::THREAD_SHARED);
+
+ // Report a hang; aManager->mLock IS locked. The hang will be processed
+ // off-main-thread, and will then be submitted back.
+ void ReportHang(TimeDuration aHangTime,
+ PersistedToDisk aPersistedToDisk = PersistedToDisk::No);
+ // Report a permanent hang; aManager->mLock IS locked
+ void ReportPermaHang();
+ // Called by BackgroundHangMonitor::NotifyActivity
+ void NotifyActivity() {
+ if (MOZ_UNLIKELY(!mTimer)) {
+ return;
+ }
+
+ MonitorAutoLock autoLock(mManager->mLock);
+ PROFILER_MARKER_UNTYPED(
+ "NotifyActivity", OTHER,
+ MarkerThreadId(mManager->mHangMonitorProfilerThreadId));
+
+ TimeStamp now = TimeStamp::Now();
+ if (mWaiting) {
+ mWaiting = false;
+ } else if (mHanging) {
+ // A hang ended.
+ ReportHang(now - mHangStart);
+ mHanging = false;
+ }
+ mLastActivity = now;
+ BackgroundHangManager::sInstance->CollectCPUUsage(now);
+
+ // Set or reset the timer.
+ mExpectedTimerNotification = now + mTimeout;
+ if (mTimeout != TimeDuration::Forever()) {
+ mTimer->InitHighResolutionWithCallback(this, mTimeout,
+ nsITimer::TYPE_ONE_SHOT);
+ }
+ }
+ // Called by BackgroundHangMonitor::NotifyWait
+ void NotifyWait() {
+ if (MOZ_UNLIKELY(!mTimer)) {
+ return;
+ }
+
+ MonitorAutoLock autoLock(mManager->mLock);
+ PROFILER_MARKER_UNTYPED(
+ "NotifyWait", OTHER,
+ MarkerThreadId(mManager->mHangMonitorProfilerThreadId));
+
+ if (mWaiting) {
+ return;
+ }
+
+ mTimer->Cancel();
+
+ mLastActivity = TimeStamp::Now();
+
+ if (mHanging) {
+ // We were hanging! We're done with that now, so let's report it.
+ // ReportHang() doesn't do much work on the current thread, and is
+ // safe to call from any thread as long as we're holding the lock.
+ ReportHang(mLastActivity - mHangStart);
+ mHanging = false;
+ }
+ mWaiting = true;
+ }
+
+ // Returns true if this thread is (or might be) shared between other
+ // BackgroundHangMonitors for the monitored thread.
+ bool IsShared() {
+ return mThreadType == BackgroundHangMonitor::THREAD_SHARED;
+ }
+};
+
+NS_IMPL_ISUPPORTS(BackgroundHangThread, nsITimerCallback, nsINamed)
+
+NS_IMETHODIMP
+BackgroundHangThread::GetName(nsACString& aName) {
+ aName.AssignLiteral("BackgroundHangThread_timer");
+ return NS_OK;
+}
+
+StaticRefPtr<BackgroundHangManager> BackgroundHangManager::sInstance;
+bool BackgroundHangManager::sDisabled = false;
+
+MOZ_THREAD_LOCAL(BackgroundHangThread*) BackgroundHangThread::sTlsKey;
+bool BackgroundHangThread::sTlsKeyInitialized;
+
+BackgroundHangManager::BackgroundHangManager()
+ : mShutdown(false), mLock("BackgroundHangManager") {
+ // Save a reference to sInstance now so that the destructor is not triggered
+ // if the InitMonitorThread RunnableMethod is released before we are done.
+ sInstance = this;
+
+ DebugOnly<nsresult> rv =
+ NS_NewNamedThread("BHMgr Monitor", getter_AddRefs(mHangMonitorThread),
+ mozilla::NewRunnableMethod(
+ "BackgroundHangManager::InitMonitorThread", this,
+ &BackgroundHangManager::InitMonitorThread));
+
+ MOZ_ASSERT(NS_SUCCEEDED(rv) && mHangMonitorThread,
+ "Failed to create BHR processing thread");
+
+ rv = NS_NewNamedThread("BHMgr Processor",
+ getter_AddRefs(mHangProcessingThread));
+ MOZ_ASSERT(NS_SUCCEEDED(rv) && mHangProcessingThread,
+ "Failed to create BHR processing thread");
+}
+
+BackgroundHangManager::~BackgroundHangManager() {
+ MOZ_ASSERT(mShutdown, "Destruction without Shutdown call");
+ MOZ_ASSERT(mHangThreads.isEmpty(), "Destruction with outstanding monitors");
+ MOZ_ASSERT(mHangMonitorThread, "No monitor thread");
+ MOZ_ASSERT(mHangProcessingThread, "No processing thread");
+
+ // NS_NewNamedThread could have failed above due to resource limitation
+ if (mHangMonitorThread) {
+ // The monitor thread can only live as long as the instance lives
+ mHangMonitorThread->Shutdown();
+ }
+
+ // Similarly, NS_NewNamedThread above could have failed.
+ if (mHangProcessingThread) {
+ mHangProcessingThread->Shutdown();
+ }
+}
+
+BackgroundHangThread::BackgroundHangThread(
+ const char* aName, uint32_t aTimeoutMs, uint32_t aMaxTimeoutMs,
+ BackgroundHangMonitor::ThreadType aThreadType)
+ : mManager(BackgroundHangManager::sInstance),
+ mThreadID(PR_GetCurrentThread()),
+ mTimeout(aTimeoutMs == BackgroundHangMonitor::kNoTimeout
+ ? TimeDuration::Forever()
+ : TimeDuration::FromMilliseconds(aTimeoutMs)),
+ mMaxTimeout(aMaxTimeoutMs == BackgroundHangMonitor::kNoTimeout
+ ? TimeDuration::Forever()
+ : TimeDuration::FromMilliseconds(aMaxTimeoutMs)),
+ mHanging(false),
+ mWaiting(true),
+ mThreadType(aThreadType),
+ mThreadName(aName) {
+ if (sTlsKeyInitialized && IsShared()) {
+ sTlsKey.set(this);
+ }
+ if (mManager->mHangMonitorThread) {
+ mTimer = NS_NewTimer(mManager->mHangMonitorThread);
+ }
+ // Lock here because LinkedList is not thread-safe
+ MonitorAutoLock autoLock(mManager->mLock);
+ // Add to thread list
+ mManager->mHangThreads.insertBack(this);
+}
+
+BackgroundHangThread::~BackgroundHangThread() {
+ // Lock here because LinkedList is not thread-safe
+ MonitorAutoLock autoLock(mManager->mLock);
+ // Remove from thread list
+ remove();
+
+ // We no longer have a thread
+ if (sTlsKeyInitialized && IsShared()) {
+ sTlsKey.set(nullptr);
+ }
+}
+
+void BackgroundHangThread::ReportHang(TimeDuration aHangTime,
+ PersistedToDisk aPersistedToDisk) {
+ // Recovered from a hang; called on the monitor thread
+ // mManager->mLock IS locked
+
+ HangDetails hangDetails(aHangTime,
+ nsDependentCString(XRE_GetProcessTypeString()),
+ NOT_REMOTE_TYPE, mThreadName, mRunnableName,
+ std::move(mHangStack), std::move(mAnnotations));
+
+ PersistedToDisk persistedToDisk = aPersistedToDisk;
+ if (aPersistedToDisk == PersistedToDisk::Yes && XRE_IsParentProcess() &&
+ mManager->mPermahangFile) {
+ auto res = WriteHangDetailsToFile(hangDetails, mManager->mPermahangFile);
+ persistedToDisk = res.isOk() ? PersistedToDisk::Yes : PersistedToDisk::No;
+ }
+
+ // If the hang processing thread exists, we can process the native stack
+ // on it. Otherwise, we are unable to report a native stack, so we just
+ // report without one.
+ if (mManager->mHangProcessingThread) {
+ nsCOMPtr<nsIRunnable> processHangStackRunnable =
+ new ProcessHangStackRunnable(std::move(hangDetails), persistedToDisk);
+ mManager->mHangProcessingThread->Dispatch(
+ processHangStackRunnable.forget());
+ } else {
+ NS_WARNING("Unable to report native stack without a BHR processing thread");
+ RefPtr<nsHangDetails> hd =
+ new nsHangDetails(std::move(hangDetails), persistedToDisk);
+ hd->Submit();
+ }
+
+ // If the profiler is enabled, add a marker.
+#ifdef MOZ_GECKO_PROFILER
+ if (profiler_thread_is_being_profiled_for_markers(
+ mStackHelper.GetThreadId())) {
+ struct HangMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("BHR-detected hang");
+ }
+ static void StreamJSONMarkerData(
+ baseprofiler::SpliceableJSONWriter& aWriter) {}
+ static MarkerSchema MarkerTypeDisplay() {
+ using MS = MarkerSchema;
+ MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable};
+ return schema;
+ }
+ };
+
+ const TimeStamp endTime = TimeStamp::Now();
+ const TimeStamp startTime = endTime - aHangTime;
+ profiler_add_marker("BHR-detected hang", geckoprofiler::category::OTHER,
+ {MarkerThreadId(mStackHelper.GetThreadId()),
+ MarkerTiming::Interval(startTime, endTime)},
+ HangMarker{});
+ }
+#endif
+}
+
+void BackgroundHangThread::ReportPermaHang() {
+ // Permanently hanged; called on the monitor thread
+ // mManager->mLock IS locked
+
+ // The significance of a permahang is that it's likely that we won't ever
+ // recover and be allowed to submit this hang. On the parent thread, we
+ // compensate for this by writing the hang details to disk on this thread,
+ // and in our next session we'll try to read those details
+ ReportHang(mMaxTimeout, PersistedToDisk::Yes);
+}
+
+NS_IMETHODIMP BackgroundHangThread::Notify(nsITimer* aTimer) {
+ MOZ_ASSERT(profiler_current_thread_id() ==
+ mManager->mHangMonitorProfilerThreadId);
+
+ MonitorAutoLock autoLock(mManager->mLock);
+ PROFILER_MARKER_UNTYPED("TimerNotify", OTHER, {});
+
+ TimeStamp now = TimeStamp::Now();
+ if (MOZ_UNLIKELY((now - mExpectedTimerNotification) * 2 > mTimeout)) {
+ // If the timer notification has been delayed by more than half the timeout
+ // time, assume the machine is not scheduling tasks correctly and ignore
+ // this hang.
+ mWaiting = true;
+ mHanging = false;
+ return NS_OK;
+ }
+
+ TimeDuration hangTime = now - mLastActivity;
+ if (MOZ_UNLIKELY(hangTime >= mMaxTimeout)) {
+ // A permahang started. No point in trying to find its exact
+ // duration, so avoid restarting the timer until there is new
+ // activity.
+ mWaiting = true;
+ mHanging = false;
+ ReportPermaHang();
+ return NS_OK;
+ }
+
+ if (MOZ_LIKELY(!mHanging && hangTime >= mTimeout)) {
+#ifdef MOZ_GECKO_PROFILER
+ // A hang started, collect a stack
+ mStackHelper.GetStack(mHangStack, mRunnableName, true);
+#endif
+
+ // If we hang immediately on waking, then the most recently collected
+ // CPU usage is going to be an average across the whole time we were
+ // sleeping. Accordingly, we want to make sure that when we hang, we
+ // collect a fresh value.
+ BackgroundHangManager::sInstance->CollectCPUUsage(now, true);
+
+ mHangStart = mLastActivity;
+ mHanging = true;
+ mAnnotations = mAnnotators.GatherAnnotations();
+ }
+
+ TimeDuration nextRecheck = mMaxTimeout - hangTime;
+ mExpectedTimerNotification = now + nextRecheck;
+ return mTimer->InitHighResolutionWithCallback(this, nextRecheck,
+ nsITimer::TYPE_ONE_SHOT);
+}
+
+BackgroundHangThread* BackgroundHangThread::FindThread() {
+#ifdef MOZ_ENABLE_BACKGROUND_HANG_MONITOR
+ if (BackgroundHangManager::sInstance == nullptr) {
+ MOZ_ASSERT(BackgroundHangManager::sDisabled,
+ "BackgroundHandleManager is not initialized");
+ return nullptr;
+ }
+
+ if (sTlsKeyInitialized) {
+ // Use TLS if available
+ return sTlsKey.get();
+ }
+ // If TLS is unavailable, we can search through the thread list
+ RefPtr<BackgroundHangManager> manager(BackgroundHangManager::sInstance);
+ MOZ_ASSERT(manager, "Creating BackgroundHangMonitor after shutdown");
+
+ PRThread* threadID = PR_GetCurrentThread();
+ // Lock thread list for traversal
+ MonitorAutoLock autoLock(manager->mLock);
+ for (BackgroundHangThread* thread = manager->mHangThreads.getFirst(); thread;
+ thread = thread->getNext()) {
+ if (thread->mThreadID == threadID && thread->IsShared()) {
+ return thread;
+ }
+ }
+#endif
+ // Current thread is not initialized
+ return nullptr;
+}
+
+bool BackgroundHangMonitor::ShouldDisableOnBeta(const nsCString& clientID) {
+ MOZ_ASSERT(clientID.Length() == 36, "clientID is invalid");
+ const char* suffix = clientID.get() + clientID.Length() - 4;
+ return strtol(suffix, NULL, 16) % BHR_BETA_MOD;
+}
+
+bool BackgroundHangMonitor::DisableOnBeta() {
+ nsAutoCString clientID;
+ nsresult rv =
+ Preferences::GetCString("toolkit.telemetry.cachedClientID", clientID);
+ bool telemetryEnabled = Telemetry::CanRecordPrereleaseData();
+
+ if (!telemetryEnabled || NS_FAILED(rv) ||
+ BackgroundHangMonitor::ShouldDisableOnBeta(clientID)) {
+ if (XRE_IsParentProcess()) {
+ BackgroundHangMonitor::Shutdown();
+ } else {
+ BackgroundHangManager::sDisabled = true;
+ }
+ return true;
+ }
+
+ return false;
+}
+
+void BackgroundHangMonitor::Startup() {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+#ifdef MOZ_ENABLE_BACKGROUND_HANG_MONITOR
+ MOZ_ASSERT(!BackgroundHangManager::sInstance, "Already initialized");
+
+ if (XRE_IsContentProcess() &&
+ StaticPrefs::toolkit_content_background_hang_monitor_disabled()) {
+ BackgroundHangManager::sDisabled = true;
+ return;
+ }
+
+# if defined(MOZ_GECKO_PROFILER) && defined(XP_WIN)
+# if defined(_M_AMD64) || defined(_M_ARM64)
+ mozilla::WindowsStackWalkInitialization();
+# endif // _M_AMD64 || _M_ARM64
+# endif // MOZ_GECKO_PROFILER && XP_WIN
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(observerService);
+
+# ifdef __clang__
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wunreachable-code"
+# endif
+ if constexpr (std::string_view(MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL)) == "beta") {
+ if (XRE_IsParentProcess()) { // cached ClientID hasn't been read yet
+ BackgroundHangThread::Startup();
+ new BackgroundHangManager();
+ Unused << NS_WARN_IF(
+ BackgroundHangManager::sInstance->mCPUUsageWatcher.Init().isErr());
+ observerService->AddObserver(BackgroundHangManager::sInstance,
+ "profile-after-change", false);
+ return;
+ } else if (DisableOnBeta()) {
+ return;
+ }
+ }
+# ifdef __clang__
+# pragma clang diagnostic pop
+# endif
+
+ BackgroundHangThread::Startup();
+ new BackgroundHangManager();
+ Unused << NS_WARN_IF(
+ BackgroundHangManager::sInstance->mCPUUsageWatcher.Init().isErr());
+ if (XRE_IsParentProcess()) {
+ observerService->AddObserver(BackgroundHangManager::sInstance,
+ "browser-delayed-startup-finished", false);
+ }
+#endif
+}
+
+void BackgroundHangMonitor::Shutdown() {
+#ifdef MOZ_ENABLE_BACKGROUND_HANG_MONITOR
+ if (BackgroundHangManager::sDisabled) {
+ MOZ_ASSERT(!BackgroundHangManager::sInstance, "Initialized");
+ return;
+ }
+
+ MOZ_ASSERT(BackgroundHangManager::sInstance, "Not initialized");
+ BackgroundHangManager::sInstance->mCPUUsageWatcher.Uninit();
+ /* Scope our lock inside Shutdown() because the sInstance object can
+ be destroyed as soon as we set sInstance to nullptr below, and
+ we don't want to hold the lock when it's being destroyed. */
+ BackgroundHangManager::sInstance->Shutdown();
+ BackgroundHangManager::sInstance = nullptr;
+ BackgroundHangManager::sDisabled = true;
+#endif
+}
+
+BackgroundHangMonitor::BackgroundHangMonitor(const char* aName,
+ uint32_t aTimeoutMs,
+ uint32_t aMaxTimeoutMs,
+ ThreadType aThreadType)
+ : mThread(aThreadType == THREAD_SHARED ? BackgroundHangThread::FindThread()
+ : nullptr) {
+#ifdef MOZ_ENABLE_BACKGROUND_HANG_MONITOR
+# ifdef MOZ_VALGRIND
+ // If we're running on Valgrind, we'll be making forward progress at a
+ // rate of somewhere between 1/25th and 1/50th of normal. This causes the
+ // BHR to capture a lot of stacks, which slows us down even more. As an
+ // attempt to avoid the worst of this, scale up all presented timeouts by
+ // a factor of thirty, and add six seconds so as to impose a six second
+ // floor on all timeouts. For a non-Valgrind-enabled build, or for an
+ // enabled build which isn't running on Valgrind, the timeouts are
+ // unchanged.
+ if (RUNNING_ON_VALGRIND) {
+ const uint32_t scaleUp = 30;
+ const uint32_t extraMs = 6000;
+ if (aTimeoutMs != BackgroundHangMonitor::kNoTimeout) {
+ aTimeoutMs *= scaleUp;
+ aTimeoutMs += extraMs;
+ }
+ if (aMaxTimeoutMs != BackgroundHangMonitor::kNoTimeout) {
+ aMaxTimeoutMs *= scaleUp;
+ aMaxTimeoutMs += extraMs;
+ }
+ }
+# endif
+
+ if (!BackgroundHangManager::sDisabled && !mThread) {
+ mThread =
+ new BackgroundHangThread(aName, aTimeoutMs, aMaxTimeoutMs, aThreadType);
+ }
+#endif
+}
+
+BackgroundHangMonitor::BackgroundHangMonitor()
+ : mThread(BackgroundHangThread::FindThread()) {
+#ifdef MOZ_ENABLE_BACKGROUND_HANG_MONITOR
+ if (BackgroundHangManager::sDisabled) {
+ return;
+ }
+#endif
+}
+
+BackgroundHangMonitor::~BackgroundHangMonitor() = default;
+
+void BackgroundHangMonitor::NotifyActivity() {
+#ifdef MOZ_ENABLE_BACKGROUND_HANG_MONITOR
+ if (mThread == nullptr) {
+ MOZ_ASSERT(BackgroundHangManager::sDisabled,
+ "This thread is not initialized for hang monitoring");
+ return;
+ }
+
+ if (Telemetry::CanRecordExtended()) {
+ mThread->NotifyActivity();
+ }
+#endif
+}
+
+void BackgroundHangMonitor::NotifyWait() {
+#ifdef MOZ_ENABLE_BACKGROUND_HANG_MONITOR
+ if (mThread == nullptr) {
+ MOZ_ASSERT(BackgroundHangManager::sDisabled,
+ "This thread is not initialized for hang monitoring");
+ return;
+ }
+
+ if (Telemetry::CanRecordExtended()) {
+ mThread->NotifyWait();
+ }
+#endif
+}
+
+bool BackgroundHangMonitor::RegisterAnnotator(
+ BackgroundHangAnnotator& aAnnotator) {
+#ifdef MOZ_ENABLE_BACKGROUND_HANG_MONITOR
+ BackgroundHangThread* thisThread = BackgroundHangThread::FindThread();
+ if (!thisThread) {
+ return false;
+ }
+ return thisThread->mAnnotators.Register(aAnnotator);
+#else
+ return false;
+#endif
+}
+
+bool BackgroundHangMonitor::UnregisterAnnotator(
+ BackgroundHangAnnotator& aAnnotator) {
+#ifdef MOZ_ENABLE_BACKGROUND_HANG_MONITOR
+ BackgroundHangThread* thisThread = BackgroundHangThread::FindThread();
+ if (!thisThread) {
+ return false;
+ }
+ return thisThread->mAnnotators.Unregister(aAnnotator);
+#else
+ return false;
+#endif
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/backgroundhangmonitor/BackgroundHangMonitor.h b/toolkit/components/backgroundhangmonitor/BackgroundHangMonitor.h
new file mode 100644
index 0000000000..037ea5e52d
--- /dev/null
+++ b/toolkit/components/backgroundhangmonitor/BackgroundHangMonitor.h
@@ -0,0 +1,198 @@
+/* -*- 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 mozilla_BackgroundHangMonitor_h
+#define mozilla_BackgroundHangMonitor_h
+
+#include "mozilla/CPUUsageWatcher.h"
+#include "mozilla/HangAnnotations.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/RefPtr.h"
+
+#include "nsString.h"
+
+#include <stdint.h>
+
+namespace mozilla {
+
+class BackgroundHangThread;
+class BackgroundHangManager;
+
+/**
+ * The background hang monitor is responsible for detecting and reporting
+ * hangs in main and background threads. A thread registers itself using
+ * the BackgroundHangMonitor object and periodically calls its methods to
+ * inform the hang monitor of the thread's activity. Each thread is given
+ * a thread name, a timeout, and a maximum timeout. If one of the thread's
+ * tasks runs for longer than the timeout duration but shorter than the
+ * maximum timeout, a (transient) hang is reported. On the other hand, if
+ * a task runs for longer than the maximum timeout duration or never
+ * finishes (e.g. in a deadlock), a permahang is reported.
+ *
+ * Tasks are defined arbitrarily, but are typically represented by events
+ * in an event loop -- processing one event is equivalent to running one
+ * task. To ensure responsiveness, tasks in a thread often have a target
+ * running time. This is a good starting point for determining the timeout
+ * and maximum timeout values. For example, the Compositor thread has a
+ * responsiveness goal of 60Hz or 17ms, so a starting timeout could be
+ * 100ms. Considering some platforms (e.g. Android) can terminate the app
+ * when a critical thread hangs for longer than a few seconds, a good
+ * starting maximum timeout is 4 or 5 seconds.
+ *
+ * A thread registers itself through the BackgroundHangMonitor constructor.
+ * Multiple BackgroundHangMonitor objects can be used in one thread. The
+ * constructor without arguments can be used when it is known that the thread
+ * already has a BackgroundHangMonitor registered. When all instances of
+ * BackgroundHangMonitor are destroyed, the thread is unregistered.
+ *
+ * The thread then uses two methods to inform BackgroundHangMonitor of the
+ * thread's activity:
+ *
+ * > BackgroundHangMonitor::NotifyActivity should be called *before*
+ * starting a task. The task run time is determined by the interval
+ * between this call and the next NotifyActivity call.
+ *
+ * > BackgroundHangMonitor::NotifyWait should be called *before* the
+ * thread enters a wait state (e.g. to wait for a new event). This
+ * prevents a waiting thread from being detected as hanging. The wait
+ * state is automatically cleared at the next NotifyActivity call.
+ *
+ * The following example shows hang monitoring in a simple event loop:
+ *
+ * void thread_main()
+ * {
+ * mozilla::BackgroundHangMonitor hangMonitor("example1", 100, 1000);
+ * while (!exiting) {
+ * hangMonitor.NotifyActivity();
+ * process_next_event();
+ * hangMonitor.NotifyWait();
+ * wait_for_next_event();
+ * }
+ * }
+ *
+ * The following example shows reentrancy in nested event loops:
+ *
+ * void thread_main()
+ * {
+ * mozilla::BackgroundHangMonitor hangMonitor("example2", 100, 1000);
+ * while (!exiting) {
+ * hangMonitor.NotifyActivity();
+ * process_next_event();
+ * hangMonitor.NotifyWait();
+ * wait_for_next_event();
+ * }
+ * }
+ *
+ * void process_next_event()
+ * {
+ * mozilla::BackgroundHangMonitor hangMonitor();
+ * if (is_sync_event) {
+ * while (!finished_event) {
+ * hangMonitor.NotifyActivity();
+ * process_next_event();
+ * hangMonitor.NotifyWait();
+ * wait_for_next_event();
+ * }
+ * } else {
+ * process_nonsync_event();
+ * }
+ * }
+ */
+class BackgroundHangMonitor {
+ private:
+ friend BackgroundHangManager;
+
+ RefPtr<BackgroundHangThread> mThread;
+
+ static bool ShouldDisableOnBeta(const nsCString&);
+ static bool DisableOnBeta();
+
+ public:
+ static const uint32_t kNoTimeout = 0;
+ enum ThreadType {
+ // For a new BackgroundHangMonitor for thread T, only create a new
+ // monitoring thread for T if one doesn't already exist. If one does,
+ // share that pre-existing monitoring thread.
+ THREAD_SHARED,
+ // For a new BackgroundHangMonitor for thread T, create a new
+ // monitoring thread for T even if there are other, pre-existing
+ // monitoring threads for T.
+ THREAD_PRIVATE
+ };
+
+ /**
+ * Enable hang monitoring.
+ * Must return before using BackgroundHangMonitor.
+ */
+ static void Startup();
+
+ /**
+ * Disable hang monitoring.
+ * Can be called without destroying all BackgroundHangMonitors first.
+ */
+ static void Shutdown();
+
+ /**
+ * Start monitoring hangs for the current thread.
+ *
+ * @param aName Name to identify the thread with
+ * @param aTimeoutMs Amount of time in milliseconds without
+ * activity before registering a hang
+ * @param aMaxTimeoutMs Amount of time in milliseconds without
+ * activity before registering a permanent hang
+ * @param aThreadType
+ * The ThreadType type of monitoring thread that should be created
+ * for this monitor. See the documentation for ThreadType.
+ */
+ BackgroundHangMonitor(const char* aName, uint32_t aTimeoutMs,
+ uint32_t aMaxTimeoutMs,
+ ThreadType aThreadType = THREAD_SHARED);
+
+ /**
+ * Monitor hangs using an existing monitor
+ * associated with the current thread.
+ */
+ BackgroundHangMonitor();
+
+ /**
+ * Destroys the hang monitor; hang monitoring for a thread stops
+ * when all monitors associated with the thread are destroyed.
+ */
+ ~BackgroundHangMonitor();
+
+ /**
+ * Notify the hang monitor of pending current thread activity.
+ * Call this method before starting an "activity" or after
+ * exiting from a wait state.
+ */
+ void NotifyActivity();
+
+ /**
+ * Notify the hang monitor of current thread wait.
+ * Call this method before entering a wait state; call
+ * NotifyActivity when subsequently exiting the wait state.
+ */
+ void NotifyWait();
+
+ /**
+ * Register an annotator with BHR for the current thread.
+ * @param aAnnotator annotator to register
+ * @return true if the annotator was registered, otherwise false.
+ */
+ static bool RegisterAnnotator(BackgroundHangAnnotator& aAnnotator);
+
+ /**
+ * Unregister an annotator that was previously registered via
+ * RegisterAnnotator.
+ * @param aAnnotator annotator to unregister
+ * @return true if there are still remaining annotators registered
+ */
+ static bool UnregisterAnnotator(BackgroundHangAnnotator& aAnnotator);
+};
+
+} // namespace mozilla
+
+#endif // mozilla_BackgroundHangMonitor_h
diff --git a/toolkit/components/backgroundhangmonitor/HangAnnotations.cpp b/toolkit/components/backgroundhangmonitor/HangAnnotations.cpp
new file mode 100644
index 0000000000..a8093c4781
--- /dev/null
+++ b/toolkit/components/backgroundhangmonitor/HangAnnotations.cpp
@@ -0,0 +1,89 @@
+/* -*- 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 "mozilla/HangAnnotations.h"
+
+#include <vector>
+
+#include "MainThreadUtils.h"
+#include "mozilla/DebugOnly.h"
+#include "nsXULAppAPI.h"
+#include "mozilla/BackgroundHangMonitor.h"
+
+namespace mozilla {
+
+void BackgroundHangAnnotations::AddAnnotation(const nsString& aName,
+ const int32_t aData) {
+ nsAutoString dataString;
+ dataString.AppendInt(aData);
+ AppendElement(HangAnnotation(aName, dataString));
+}
+
+void BackgroundHangAnnotations::AddAnnotation(const nsString& aName,
+ const double aData) {
+ nsAutoString dataString;
+ dataString.AppendFloat(aData);
+ AppendElement(HangAnnotation(aName, dataString));
+}
+
+void BackgroundHangAnnotations::AddAnnotation(const nsString& aName,
+ const nsString& aData) {
+ AppendElement(HangAnnotation(aName, aData));
+}
+
+void BackgroundHangAnnotations::AddAnnotation(const nsString& aName,
+ const nsCString& aData) {
+ NS_ConvertUTF8toUTF16 dataString(aData);
+ AppendElement(HangAnnotation(aName, dataString));
+}
+
+void BackgroundHangAnnotations::AddAnnotation(const nsString& aName,
+ const bool aData) {
+ if (aData) {
+ AppendElement(HangAnnotation(aName, u"true"_ns));
+ } else {
+ AppendElement(HangAnnotation(aName, u"false"_ns));
+ }
+}
+
+BackgroundHangAnnotators::BackgroundHangAnnotators()
+ : mMutex("BackgroundHangAnnotators::mMutex") {
+ MOZ_COUNT_CTOR(BackgroundHangAnnotators);
+}
+
+BackgroundHangAnnotators::~BackgroundHangAnnotators() {
+ MOZ_ASSERT(mAnnotators.empty());
+ MOZ_COUNT_DTOR(BackgroundHangAnnotators);
+}
+
+bool BackgroundHangAnnotators::Register(BackgroundHangAnnotator& aAnnotator) {
+ MutexAutoLock lock(mMutex);
+ auto result = mAnnotators.insert(&aAnnotator);
+ return result.second;
+}
+
+bool BackgroundHangAnnotators::Unregister(BackgroundHangAnnotator& aAnnotator) {
+ MutexAutoLock lock(mMutex);
+ DebugOnly<std::set<BackgroundHangAnnotator*>::size_type> numErased;
+ numErased = mAnnotators.erase(&aAnnotator);
+ MOZ_ASSERT(numErased == 1);
+ return mAnnotators.empty();
+}
+
+BackgroundHangAnnotations BackgroundHangAnnotators::GatherAnnotations() {
+ BackgroundHangAnnotations annotations;
+ { // Scope for lock
+ MutexAutoLock lock(mMutex);
+ for (std::set<BackgroundHangAnnotator*>::iterator i = mAnnotators.begin(),
+ e = mAnnotators.end();
+ i != e; ++i) {
+ (*i)->AnnotateHang(annotations);
+ }
+ }
+ return annotations;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/backgroundhangmonitor/HangAnnotations.h b/toolkit/components/backgroundhangmonitor/HangAnnotations.h
new file mode 100644
index 0000000000..f6667efa42
--- /dev/null
+++ b/toolkit/components/backgroundhangmonitor/HangAnnotations.h
@@ -0,0 +1,71 @@
+/* -*- 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 mozilla_HangAnnotations_h
+#define mozilla_HangAnnotations_h
+
+#include <set>
+
+#include "mozilla/HangTypes.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/Vector.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "mozilla/ipc/IPDLParamTraits.h"
+
+namespace mozilla {
+
+/**
+ * This class extends nsTArray<HangAnnotation> with some methods for adding
+ * annotations being reported by a registered hang Annotator.
+ */
+class BackgroundHangAnnotations : public nsTArray<HangAnnotation> {
+ public:
+ void AddAnnotation(const nsString& aName, const int32_t aData);
+ void AddAnnotation(const nsString& aName, const double aData);
+ void AddAnnotation(const nsString& aName, const nsString& aData);
+ void AddAnnotation(const nsString& aName, const nsCString& aData);
+ void AddAnnotation(const nsString& aName, const bool aData);
+};
+
+class BackgroundHangAnnotator {
+ public:
+ /**
+ * NB: This function is always called by the BackgroundHangMonitor thread.
+ * Plan accordingly.
+ */
+ virtual void AnnotateHang(BackgroundHangAnnotations& aAnnotations) = 0;
+};
+
+class BackgroundHangAnnotators {
+ public:
+ BackgroundHangAnnotators();
+ ~BackgroundHangAnnotators();
+
+ bool Register(BackgroundHangAnnotator& aAnnotator);
+ bool Unregister(BackgroundHangAnnotator& aAnnotator);
+
+ BackgroundHangAnnotations GatherAnnotations();
+
+ private:
+ Mutex mMutex MOZ_UNANNOTATED;
+ std::set<BackgroundHangAnnotator*> mAnnotators;
+};
+
+namespace ipc {
+
+template <>
+struct IPDLParamTraits<mozilla::BackgroundHangAnnotations>
+ : public IPDLParamTraits<nsTArray<mozilla::HangAnnotation>> {
+ typedef mozilla::BackgroundHangAnnotations paramType;
+};
+
+} // namespace ipc
+
+} // namespace mozilla
+
+#endif // mozilla_HangAnnotations_h
diff --git a/toolkit/components/backgroundhangmonitor/HangDetails.cpp b/toolkit/components/backgroundhangmonitor/HangDetails.cpp
new file mode 100644
index 0000000000..de6ee056f6
--- /dev/null
+++ b/toolkit/components/backgroundhangmonitor/HangDetails.cpp
@@ -0,0 +1,737 @@
+/* -*- 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 "HangDetails.h"
+
+#include "nsIHangDetails.h"
+#include "nsPrintfCString.h"
+#include "js/Array.h" // JS::NewArrayObject
+#include "js/PropertyAndElement.h" // JS_DefineElement
+#include "mozilla/FileUtils.h"
+#include "mozilla/gfx/GPUParent.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h" // For RemoteTypePrefix
+#include "mozilla/FileUtils.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/Unused.h"
+#include "mozilla/GfxMessageUtils.h" // For ParamTraits<GeckoProcessType>
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/Try.h"
+#include "shared-libraries.h"
+
+static const char MAGIC[] = "permahangsavev1";
+
+namespace mozilla {
+
+NS_IMETHODIMP
+nsHangDetails::GetWasPersisted(bool* aWasPersisted) {
+ *aWasPersisted = mPersistedToDisk == PersistedToDisk::Yes;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsHangDetails::GetDuration(double* aDuration) {
+ *aDuration = mDetails.duration().ToMilliseconds();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsHangDetails::GetThread(nsACString& aName) {
+ aName.Assign(mDetails.threadName());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsHangDetails::GetRunnableName(nsACString& aRunnableName) {
+ aRunnableName.Assign(mDetails.runnableName());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsHangDetails::GetProcess(nsACString& aName) {
+ aName.Assign(mDetails.process());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsHangDetails::GetRemoteType(nsACString& aName) {
+ aName.Assign(mDetails.remoteType());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsHangDetails::GetAnnotations(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aVal) {
+ // We create an Array with ["key", "value"] string pair entries for each item
+ // in our annotations object.
+ auto& annotations = mDetails.annotations();
+ size_t length = annotations.Length();
+ JS::Rooted<JSObject*> retObj(aCx, JS::NewArrayObject(aCx, length));
+ if (!retObj) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ for (size_t i = 0; i < length; ++i) {
+ const auto& annotation = annotations[i];
+ JS::Rooted<JSObject*> annotationPair(aCx, JS::NewArrayObject(aCx, 2));
+ if (!annotationPair) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ JS::Rooted<JSString*> key(aCx,
+ JS_NewUCStringCopyN(aCx, annotation.name().get(),
+ annotation.name().Length()));
+ if (!key) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ JS::Rooted<JSString*> value(
+ aCx, JS_NewUCStringCopyN(aCx, annotation.value().get(),
+ annotation.value().Length()));
+ if (!value) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (!JS_DefineElement(aCx, annotationPair, 0, key, JSPROP_ENUMERATE)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (!JS_DefineElement(aCx, annotationPair, 1, value, JSPROP_ENUMERATE)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (!JS_DefineElement(aCx, retObj, i, annotationPair, JSPROP_ENUMERATE)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ aVal.setObject(*retObj);
+ return NS_OK;
+}
+
+namespace {
+
+nsresult StringFrame(JSContext* aCx, JS::RootedObject& aTarget, size_t aIndex,
+ const char* aString) {
+ JSString* jsString = JS_NewStringCopyZ(aCx, aString);
+ if (!jsString) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ JS::Rooted<JSString*> string(aCx, jsString);
+ if (!string) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ if (!JS_DefineElement(aCx, aTarget, aIndex, string, JSPROP_ENUMERATE)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+}
+
+} // anonymous namespace
+
+NS_IMETHODIMP
+nsHangDetails::GetStack(JSContext* aCx, JS::MutableHandle<JS::Value> aStack) {
+ auto& stack = mDetails.stack();
+ uint32_t length = stack.stack().Length();
+ JS::Rooted<JSObject*> ret(aCx, JS::NewArrayObject(aCx, length));
+ if (!ret) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ for (uint32_t i = 0; i < length; ++i) {
+ auto& entry = stack.stack()[i];
+ switch (entry.type()) {
+ case HangEntry::TnsCString: {
+ nsresult rv = StringFrame(aCx, ret, i, entry.get_nsCString().get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ }
+ case HangEntry::THangEntryBufOffset: {
+ uint32_t offset = entry.get_HangEntryBufOffset().index();
+
+ // NOTE: We can't trust the offset we got, as we might have gotten it
+ // from a compromised content process. Validate that it is in bounds.
+ if (NS_WARN_IF(stack.strbuffer().IsEmpty() ||
+ offset >= stack.strbuffer().Length())) {
+ MOZ_ASSERT_UNREACHABLE("Corrupted offset data");
+ return NS_ERROR_FAILURE;
+ }
+
+ // NOTE: If our content process is compromised, it could send us back a
+ // strbuffer() which didn't have a null terminator. If the last byte in
+ // the buffer is not '\0', we abort, to make sure we don't read out of
+ // bounds.
+ if (stack.strbuffer().LastElement() != '\0') {
+ MOZ_ASSERT_UNREACHABLE("Corrupted strbuffer data");
+ return NS_ERROR_FAILURE;
+ }
+
+ // We know this offset is safe because of the previous checks.
+ const int8_t* start = stack.strbuffer().Elements() + offset;
+ nsresult rv =
+ StringFrame(aCx, ret, i, reinterpret_cast<const char*>(start));
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ }
+ case HangEntry::THangEntryModOffset: {
+ const HangEntryModOffset& mo = entry.get_HangEntryModOffset();
+
+ JS::Rooted<JSObject*> jsFrame(aCx, JS::NewArrayObject(aCx, 2));
+ if (!jsFrame) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (!JS_DefineElement(aCx, jsFrame, 0, mo.module(), JSPROP_ENUMERATE)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ nsPrintfCString hexString("%" PRIxPTR, (uintptr_t)mo.offset());
+ JS::Rooted<JSString*> hex(aCx, JS_NewStringCopyZ(aCx, hexString.get()));
+ if (!hex || !JS_DefineElement(aCx, jsFrame, 1, hex, JSPROP_ENUMERATE)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (!JS_DefineElement(aCx, ret, i, jsFrame, JSPROP_ENUMERATE)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ break;
+ }
+ case HangEntry::THangEntryProgCounter: {
+ // Don't bother recording fixed program counters to JS
+ nsresult rv = StringFrame(aCx, ret, i, "(unresolved)");
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ }
+ case HangEntry::THangEntryContent: {
+ nsresult rv = StringFrame(aCx, ret, i, "(content script)");
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ }
+ case HangEntry::THangEntryJit: {
+ nsresult rv = StringFrame(aCx, ret, i, "(jit frame)");
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ }
+ case HangEntry::THangEntryWasm: {
+ nsresult rv = StringFrame(aCx, ret, i, "(wasm)");
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ }
+ case HangEntry::THangEntryChromeScript: {
+ nsresult rv = StringFrame(aCx, ret, i, "(chrome script)");
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ }
+ case HangEntry::THangEntrySuppressed: {
+ nsresult rv = StringFrame(aCx, ret, i, "(profiling suppressed)");
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ }
+ default:
+ MOZ_CRASH("Unsupported HangEntry type?");
+ }
+ }
+
+ aStack.setObject(*ret);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsHangDetails::GetModules(JSContext* aCx, JS::MutableHandle<JS::Value> aVal) {
+ auto& modules = mDetails.stack().modules();
+ size_t length = modules.Length();
+ JS::Rooted<JSObject*> retObj(aCx, JS::NewArrayObject(aCx, length));
+ if (!retObj) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ for (size_t i = 0; i < length; ++i) {
+ const HangModule& module = modules[i];
+ JS::Rooted<JSObject*> jsModule(aCx, JS::NewArrayObject(aCx, 2));
+ if (!jsModule) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ JS::Rooted<JSString*> name(
+ aCx, JS_NewUCStringCopyN(aCx, module.name().BeginReading(),
+ module.name().Length()));
+ if (!JS_DefineElement(aCx, jsModule, 0, name, JSPROP_ENUMERATE)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ JS::Rooted<JSString*> breakpadId(
+ aCx, JS_NewStringCopyN(aCx, module.breakpadId().BeginReading(),
+ module.breakpadId().Length()));
+ if (!JS_DefineElement(aCx, jsModule, 1, breakpadId, JSPROP_ENUMERATE)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (!JS_DefineElement(aCx, retObj, i, jsModule, JSPROP_ENUMERATE)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ aVal.setObject(*retObj);
+ return NS_OK;
+}
+
+// Processing and submitting the stack as an observer notification.
+
+void nsHangDetails::Submit() {
+ RefPtr<nsHangDetails> hangDetails = this;
+ nsCOMPtr<nsIRunnable> notifyObservers =
+ NS_NewRunnableFunction("NotifyBHRHangObservers", [hangDetails] {
+ // The place we need to report the hang to varies depending on process.
+ //
+ // In child processes, we report the hang to our parent process, while
+ // if we're in the parent process, we report a bhr-thread-hang observer
+ // notification.
+ switch (XRE_GetProcessType()) {
+ case GeckoProcessType_Content: {
+ auto cc = dom::ContentChild::GetSingleton();
+ if (cc) {
+ // Use the prefix so we don't get URIs from Fission isolated
+ // processes.
+ hangDetails->mDetails.remoteType().Assign(
+ dom::RemoteTypePrefix(cc->GetRemoteType()));
+ Unused << cc->SendBHRThreadHang(hangDetails->mDetails);
+ }
+ break;
+ }
+ case GeckoProcessType_GPU: {
+ auto gp = gfx::GPUParent::GetSingleton();
+ if (gp) {
+ Unused << gp->SendBHRThreadHang(hangDetails->mDetails);
+ }
+ break;
+ }
+ case GeckoProcessType_Default: {
+ nsCOMPtr<nsIObserverService> os =
+ mozilla::services::GetObserverService();
+ if (os) {
+ os->NotifyObservers(hangDetails, "bhr-thread-hang", nullptr);
+ }
+ break;
+ }
+ default:
+ // XXX: Consider handling GeckoProcessType_GMPlugin and
+ // GeckoProcessType_Plugin?
+ NS_WARNING("Unsupported BHR process type - discarding hang.");
+ break;
+ }
+ });
+
+ nsresult rv = SchedulerGroup::Dispatch(notifyObservers.forget());
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+}
+
+NS_IMPL_ISUPPORTS(nsHangDetails, nsIHangDetails)
+
+namespace {
+
+// Sorting comparator used by ReadModuleInformation. Sorts PC Frames by their
+// PC.
+struct PCFrameComparator {
+ bool LessThan(HangEntry* const& a, HangEntry* const& b) const {
+ return a->get_HangEntryProgCounter().pc() <
+ b->get_HangEntryProgCounter().pc();
+ }
+ bool Equals(HangEntry* const& a, HangEntry* const& b) const {
+ return a->get_HangEntryProgCounter().pc() ==
+ b->get_HangEntryProgCounter().pc();
+ }
+};
+
+} // anonymous namespace
+
+void ReadModuleInformation(HangStack& stack) {
+ // modules() should be empty when we start filling it.
+ stack.modules().Clear();
+
+#ifdef MOZ_GECKO_PROFILER
+ // Create a sorted list of the PCs in the current stack.
+ AutoTArray<HangEntry*, 100> frames;
+ for (auto& frame : stack.stack()) {
+ if (frame.type() == HangEntry::THangEntryProgCounter) {
+ frames.AppendElement(&frame);
+ }
+ }
+ PCFrameComparator comparator;
+ frames.Sort(comparator);
+
+ SharedLibraryInfo rawModules = SharedLibraryInfo::GetInfoForSelf();
+ rawModules.SortByAddress();
+
+ size_t frameIdx = 0;
+ for (size_t i = 0; i < rawModules.GetSize(); ++i) {
+ const SharedLibrary& info = rawModules.GetEntry(i);
+ uintptr_t moduleStart = info.GetStart();
+ uintptr_t moduleEnd = info.GetEnd() - 1;
+ // the interval is [moduleStart, moduleEnd)
+
+ bool moduleReferenced = false;
+ for (; frameIdx < frames.Length(); ++frameIdx) {
+ auto& frame = frames[frameIdx];
+ uint64_t pc = frame->get_HangEntryProgCounter().pc();
+ // We've moved past this frame, let's go to the next one.
+ if (pc >= moduleEnd) {
+ break;
+ }
+ if (pc >= moduleStart) {
+ uint64_t offset = pc - moduleStart;
+ if (NS_WARN_IF(offset > UINT32_MAX)) {
+ continue; // module/offset can only hold 32-bit offsets into shared
+ // libraries.
+ }
+
+ // If we found the module, rewrite the Frame entry to instead be a
+ // ModOffset one. mModules.Length() will be the index of the module when
+ // we append it below, and we set moduleReferenced to true to ensure
+ // that we do.
+ moduleReferenced = true;
+ uint32_t module = stack.modules().Length();
+ HangEntryModOffset modOffset(module, static_cast<uint32_t>(offset));
+ *frame = modOffset;
+ }
+ }
+
+ if (moduleReferenced) {
+ HangModule module(info.GetDebugName(), info.GetBreakpadId());
+ stack.modules().AppendElement(module);
+ }
+ }
+#endif
+}
+
+Result<Ok, nsresult> ReadData(PRFileDesc* aFile, void* aPtr, size_t aLength) {
+ int32_t readResult = PR_Read(aFile, aPtr, aLength);
+ if (readResult < 0 || size_t(readResult) != aLength) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return Ok();
+}
+
+Result<Ok, nsresult> WriteData(PRFileDesc* aFile, void* aPtr, size_t aLength) {
+ int32_t writeResult = PR_Write(aFile, aPtr, aLength);
+ if (writeResult < 0 || size_t(writeResult) != aLength) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return Ok();
+}
+
+Result<Ok, nsresult> WriteUint(PRFileDesc* aFile, const CheckedUint32& aInt) {
+ if (!aInt.isValid()) {
+ MOZ_ASSERT_UNREACHABLE("Integer value out of bounds.");
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+ int32_t value = aInt.value();
+ MOZ_TRY(WriteData(aFile, (void*)&value, sizeof(value)));
+ return Ok();
+}
+
+Result<uint32_t, nsresult> ReadUint(PRFileDesc* aFile) {
+ int32_t value;
+ MOZ_TRY(ReadData(aFile, (void*)&value, sizeof(value)));
+ return value;
+}
+
+Result<Ok, nsresult> WriteCString(PRFileDesc* aFile, const char* aString) {
+ size_t length = strlen(aString);
+ MOZ_TRY(WriteUint(aFile, CheckedUint32(length)));
+ MOZ_TRY(WriteData(aFile, (void*)aString, length));
+ return Ok();
+}
+
+template <typename CharT>
+Result<Ok, nsresult> WriteTString(PRFileDesc* aFile,
+ const nsTString<CharT>& aString) {
+ MOZ_TRY(WriteUint(aFile, CheckedUint32(aString.Length())));
+ size_t size = aString.Length() * sizeof(CharT);
+ MOZ_TRY(WriteData(aFile, (void*)aString.get(), size));
+ return Ok();
+}
+
+template <typename CharT>
+Result<nsTString<CharT>, nsresult> ReadTString(PRFileDesc* aFile) {
+ uint32_t length;
+ MOZ_TRY_VAR(length, ReadUint(aFile));
+ nsTString<CharT> result;
+ CharT buffer[512];
+ size_t bufferLength = sizeof(buffer) / sizeof(CharT);
+ while (length != 0) {
+ size_t toRead = std::min(bufferLength, size_t(length));
+ size_t toReadSize = toRead * sizeof(CharT);
+ MOZ_TRY(ReadData(aFile, (void*)buffer, toReadSize));
+
+ if (!result.Append(buffer, toRead, mozilla::fallible)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (length > bufferLength) {
+ length -= bufferLength;
+ } else {
+ length = 0;
+ }
+ }
+ return result;
+}
+
+Result<Ok, nsresult> WriteEntry(PRFileDesc* aFile, const HangStack& aStack,
+ const HangEntry& aEntry) {
+ MOZ_TRY(WriteUint(aFile, uint32_t(aEntry.type())));
+ switch (aEntry.type()) {
+ case HangEntry::TnsCString: {
+ MOZ_TRY(WriteTString(aFile, aEntry.get_nsCString()));
+ break;
+ }
+ case HangEntry::THangEntryBufOffset: {
+ uint32_t offset = aEntry.get_HangEntryBufOffset().index();
+
+ if (NS_WARN_IF(aStack.strbuffer().IsEmpty() ||
+ offset >= aStack.strbuffer().Length())) {
+ MOZ_ASSERT_UNREACHABLE("Corrupted offset data");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (aStack.strbuffer().LastElement() != '\0') {
+ MOZ_ASSERT_UNREACHABLE("Corrupted strbuffer data");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ const char* start = (const char*)aStack.strbuffer().Elements() + offset;
+ MOZ_TRY(WriteCString(aFile, start));
+ break;
+ }
+ case HangEntry::THangEntryModOffset: {
+ const HangEntryModOffset& mo = aEntry.get_HangEntryModOffset();
+
+ MOZ_TRY(WriteUint(aFile, CheckedUint32(mo.module())));
+ MOZ_TRY(WriteUint(aFile, CheckedUint32(mo.offset())));
+ break;
+ }
+ case HangEntry::THangEntryProgCounter:
+ case HangEntry::THangEntryContent:
+ case HangEntry::THangEntryJit:
+ case HangEntry::THangEntryWasm:
+ case HangEntry::THangEntryChromeScript:
+ case HangEntry::THangEntrySuppressed: {
+ break;
+ }
+ default:
+ MOZ_CRASH("Unsupported HangEntry type?");
+ }
+ return Ok();
+}
+
+Result<Ok, nsresult> ReadEntry(PRFileDesc* aFile, HangStack& aStack) {
+ uint32_t type;
+ MOZ_TRY_VAR(type, ReadUint(aFile));
+ HangEntry::Type entryType = HangEntry::Type(type);
+ switch (entryType) {
+ case HangEntry::TnsCString:
+ case HangEntry::THangEntryBufOffset: {
+ nsCString str;
+ MOZ_TRY_VAR(str, ReadTString<char>(aFile));
+ aStack.stack().AppendElement(std::move(str));
+ break;
+ }
+ case HangEntry::THangEntryModOffset: {
+ uint32_t module;
+ MOZ_TRY_VAR(module, ReadUint(aFile));
+ uint32_t offset;
+ MOZ_TRY_VAR(offset, ReadUint(aFile));
+ aStack.stack().AppendElement(HangEntryModOffset(module, offset));
+ break;
+ }
+ case HangEntry::THangEntryProgCounter: {
+ aStack.stack().AppendElement(HangEntryProgCounter());
+ break;
+ }
+ case HangEntry::THangEntryContent: {
+ aStack.stack().AppendElement(HangEntryContent());
+ break;
+ }
+ case HangEntry::THangEntryJit: {
+ aStack.stack().AppendElement(HangEntryJit());
+ break;
+ }
+ case HangEntry::THangEntryWasm: {
+ aStack.stack().AppendElement(HangEntryWasm());
+ break;
+ }
+ case HangEntry::THangEntryChromeScript: {
+ aStack.stack().AppendElement(HangEntryChromeScript());
+ break;
+ }
+ case HangEntry::THangEntrySuppressed: {
+ aStack.stack().AppendElement(HangEntrySuppressed());
+ break;
+ }
+ default:
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+ return Ok();
+}
+
+Result<HangDetails, nsresult> ReadHangDetailsFromFile(nsIFile* aFile) {
+ AutoFDClose raiiFd;
+ nsresult rv =
+ aFile->OpenNSPRFileDesc(PR_RDONLY, 0644, getter_Transfers(raiiFd));
+ const auto fd = raiiFd.get();
+ if (NS_FAILED(rv)) {
+ return Err(rv);
+ }
+
+ uint8_t magicBuffer[sizeof(MAGIC)];
+ MOZ_TRY(ReadData(fd, (void*)magicBuffer, sizeof(MAGIC)));
+
+ if (memcmp(magicBuffer, MAGIC, sizeof(MAGIC)) != 0) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ HangDetails result;
+ uint32_t duration;
+ MOZ_TRY_VAR(duration, ReadUint(fd));
+ result.duration() = TimeDuration::FromMilliseconds(double(duration));
+ MOZ_TRY_VAR(result.threadName(), ReadTString<char>(fd));
+ MOZ_TRY_VAR(result.runnableName(), ReadTString<char>(fd));
+ MOZ_TRY_VAR(result.process(), ReadTString<char>(fd));
+ MOZ_TRY_VAR(result.remoteType(), ReadTString<char>(fd));
+
+ uint32_t numAnnotations;
+ MOZ_TRY_VAR(numAnnotations, ReadUint(fd));
+ auto& annotations = result.annotations();
+
+ // Add a "Unrecovered" annotation so we can know when processing this that
+ // the hang persisted until the process was closed.
+ if (!annotations.SetCapacity(numAnnotations + 1, mozilla::fallible)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ annotations.AppendElement(HangAnnotation(u"Unrecovered"_ns, u"true"_ns));
+
+ for (size_t i = 0; i < numAnnotations; ++i) {
+ HangAnnotation annot;
+ MOZ_TRY_VAR(annot.name(), ReadTString<char16_t>(fd));
+ MOZ_TRY_VAR(annot.value(), ReadTString<char16_t>(fd));
+ annotations.AppendElement(std::move(annot));
+ }
+
+ auto& stack = result.stack();
+ uint32_t numFrames;
+ MOZ_TRY_VAR(numFrames, ReadUint(fd));
+ if (!stack.stack().SetCapacity(numFrames, mozilla::fallible)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ for (size_t i = 0; i < numFrames; ++i) {
+ MOZ_TRY(ReadEntry(fd, stack));
+ }
+
+ uint32_t numModules;
+ MOZ_TRY_VAR(numModules, ReadUint(fd));
+ auto& modules = stack.modules();
+ if (!annotations.SetCapacity(numModules, mozilla::fallible)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ for (size_t i = 0; i < numModules; ++i) {
+ HangModule module;
+ MOZ_TRY_VAR(module.name(), ReadTString<char16_t>(fd));
+ MOZ_TRY_VAR(module.breakpadId(), ReadTString<char>(fd));
+ modules.AppendElement(std::move(module));
+ }
+
+ return result;
+}
+
+Result<Ok, nsresult> WriteHangDetailsToFile(HangDetails& aDetails,
+ nsIFile* aFile) {
+ if (NS_WARN_IF(!aFile)) {
+ return Err(NS_ERROR_INVALID_POINTER);
+ }
+
+ AutoFDClose raiiFd;
+ nsresult rv = aFile->OpenNSPRFileDesc(
+ PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, 0644, getter_Transfers(raiiFd));
+ const auto fd = raiiFd.get();
+
+ if (NS_FAILED(rv)) {
+ return Err(rv);
+ }
+
+ MOZ_TRY(WriteData(fd, (void*)MAGIC, sizeof(MAGIC)));
+
+ double duration = aDetails.duration().ToMilliseconds();
+ if (duration > double(std::numeric_limits<uint32_t>::max())) {
+ // Something has gone terribly wrong if we've hung for more than 2^32 ms.
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ MOZ_TRY(WriteUint(fd, uint32_t(duration)));
+ MOZ_TRY(WriteTString(fd, aDetails.threadName()));
+ MOZ_TRY(WriteTString(fd, aDetails.runnableName()));
+ MOZ_TRY(WriteTString(fd, aDetails.process()));
+ MOZ_TRY(WriteTString(fd, aDetails.remoteType()));
+ MOZ_TRY(WriteUint(fd, CheckedUint32(aDetails.annotations().Length())));
+
+ for (auto& annot : aDetails.annotations()) {
+ MOZ_TRY(WriteTString(fd, annot.name()));
+ MOZ_TRY(WriteTString(fd, annot.value()));
+ }
+
+ auto& stack = aDetails.stack();
+ ReadModuleInformation(stack);
+
+ MOZ_TRY(WriteUint(fd, CheckedUint32(stack.stack().Length())));
+ for (auto& entry : stack.stack()) {
+ MOZ_TRY(WriteEntry(fd, stack, entry));
+ }
+
+ auto& modules = stack.modules();
+ MOZ_TRY(WriteUint(fd, CheckedUint32(modules.Length())));
+
+ for (auto& module : modules) {
+ MOZ_TRY(WriteTString(fd, module.name()));
+ MOZ_TRY(WriteTString(fd, module.breakpadId()));
+ }
+
+ return Ok();
+}
+
+NS_IMETHODIMP
+ProcessHangStackRunnable::Run() {
+ // NOTE: Reading module information can take a long time, which is why we do
+ // it off-main-thread.
+ if (mHangDetails.stack().modules().IsEmpty()) {
+ ReadModuleInformation(mHangDetails.stack());
+ }
+
+ RefPtr<nsHangDetails> hangDetails =
+ new nsHangDetails(std::move(mHangDetails), mPersistedToDisk);
+ hangDetails->Submit();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SubmitPersistedPermahangRunnable::Run() {
+ auto hangDetailsResult = ReadHangDetailsFromFile(mPermahangFile);
+ if (hangDetailsResult.isErr()) {
+ // If we somehow failed in trying to deserialize the hang file, go ahead
+ // and delete it to prevent future runs from having to go through the
+ // same thing. If we succeeded, however, the file should be cleaned up
+ // once the hang is submitted.
+ Unused << mPermahangFile->Remove(false);
+ return hangDetailsResult.unwrapErr();
+ }
+ RefPtr<nsHangDetails> hangDetails =
+ new nsHangDetails(hangDetailsResult.unwrap(), PersistedToDisk::Yes);
+ hangDetails->Submit();
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/backgroundhangmonitor/HangDetails.h b/toolkit/components/backgroundhangmonitor/HangDetails.h
new file mode 100644
index 0000000000..8641bfcb71
--- /dev/null
+++ b/toolkit/components/backgroundhangmonitor/HangDetails.h
@@ -0,0 +1,101 @@
+/* -*- 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 mozilla_HangDetails_h
+#define mozilla_HangDetails_h
+
+#include <utility>
+
+#include "ipc/IPCMessageUtils.h"
+#include "mozilla/HangAnnotations.h"
+#include "mozilla/HangTypes.h"
+#include "mozilla/ProcessedStack.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Result.h"
+#include "mozilla/TimeStamp.h"
+#include "nsIFile.h"
+#include "nsIHangDetails.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+enum class PersistedToDisk {
+ No,
+ Yes,
+};
+
+/**
+ * HangDetails is the concrete implementaion of nsIHangDetails, and contains the
+ * infromation which we want to expose to observers of the bhr-thread-hang
+ * observer notification.
+ */
+class nsHangDetails : public nsIHangDetails {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIHANGDETAILS
+
+ explicit nsHangDetails(HangDetails&& aDetails,
+ PersistedToDisk aPersistedToDisk)
+ : mDetails(std::move(aDetails)), mPersistedToDisk(aPersistedToDisk) {}
+
+ // Submit these HangDetails to the main thread. This will dispatch a runnable
+ // to the main thread which will fire off the bhr-thread-hang observer
+ // notification with this HangDetails as the subject.
+ void Submit();
+
+ private:
+ virtual ~nsHangDetails() = default;
+
+ HangDetails mDetails;
+ PersistedToDisk mPersistedToDisk;
+};
+
+Result<Ok, nsresult> WriteHangDetailsToFile(HangDetails& aDetails,
+ nsIFile* aFile);
+
+/**
+ * This runnable is run on the StreamTransportService threadpool in order to
+ * process the stack off main thread before submitting it to the main thread as
+ * an observer notification.
+ *
+ * This object should have the only remaining reference to aHangDetails, as it
+ * will access its fields without synchronization.
+ */
+class ProcessHangStackRunnable final : public Runnable {
+ public:
+ explicit ProcessHangStackRunnable(HangDetails&& aHangDetails,
+ PersistedToDisk aPersistedToDisk)
+ : Runnable("ProcessHangStackRunnable"),
+ mHangDetails(std::move(aHangDetails)),
+ mPersistedToDisk(aPersistedToDisk) {}
+
+ NS_IMETHOD Run() override;
+
+ private:
+ HangDetails mHangDetails;
+ PersistedToDisk mPersistedToDisk;
+};
+
+/**
+ * This runnable handles checking whether our last session wrote a permahang to
+ * disk which we were unable to submit through telemetry. If so, we read the
+ * permahang out and try again to submit it.
+ */
+class SubmitPersistedPermahangRunnable final : public Runnable {
+ public:
+ explicit SubmitPersistedPermahangRunnable(nsIFile* aPermahangFile)
+ : Runnable("SubmitPersistedPermahangRunnable"),
+ mPermahangFile(aPermahangFile) {}
+
+ NS_IMETHOD Run() override;
+
+ private:
+ nsCOMPtr<nsIFile> mPermahangFile;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_HangDetails_h
diff --git a/toolkit/components/backgroundhangmonitor/HangTypes.ipdlh b/toolkit/components/backgroundhangmonitor/HangTypes.ipdlh
new file mode 100644
index 0000000000..e791bd6f44
--- /dev/null
+++ b/toolkit/components/backgroundhangmonitor/HangTypes.ipdlh
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
+/* vim: set sw=4 ts=8 et tw=80 ft=cpp : */
+/* 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 mozilla::TimeDuration from "mozilla/TimeStamp.h";
+
+namespace mozilla {
+
+// The different kinds of hang entries which we're going to need to handle in
+// our HangStacks.
+
+struct HangEntryBufOffset
+{
+ // NOTE: Don't trust this index without checking it is a valid index into
+ // the strbuffer, and that the buffer's last byte is a '\0'.
+ uint32_t index;
+};
+
+struct HangEntryModOffset
+{
+ uint32_t module;
+ uint32_t offset;
+};
+
+struct HangEntryProgCounter
+{
+ uintptr_t pc;
+};
+
+// Singleton structs for the union type.
+struct HangEntryContent {};
+struct HangEntryJit {};
+struct HangEntryWasm {};
+struct HangEntryChromeScript {};
+struct HangEntrySuppressed {};
+
+union HangEntry
+{
+ // String representing a pseudostack or chrome JS stack.
+ nsCString;
+ // The index of the start of a string in the associated buffer.
+ HangEntryBufOffset;
+ // A module index and offset into that module.
+ HangEntryModOffset;
+ // A raw program counter which has not been mapped into a module.
+ HangEntryProgCounter;
+ // A hidden "(content script)" frame.
+ HangEntryContent;
+ // An unprocessed "(jit frame)"
+ HangEntryJit;
+ // An unprocessed "(wasm)" frame.
+ HangEntryWasm;
+ // A chrome script which didn't fit in the buffer.
+ HangEntryChromeScript;
+ // A JS frame while profiling was suppressed.
+ HangEntrySuppressed;
+};
+
+struct HangModule
+{
+ // The file name, /foo/bar/libxul.so for example.
+ // It can contain unicode characters.
+ nsString name;
+ nsCString breakpadId;
+};
+
+struct HangStack
+{
+ HangEntry[] stack;
+ int8_t[] strbuffer;
+ HangModule[] modules;
+};
+
+// Hang annotation information.
+struct HangAnnotation
+{
+ nsString name;
+ nsString value;
+};
+
+// The information about an individual hang which is sent over IPC.
+struct HangDetails
+{
+ TimeDuration duration;
+ nsCString process;
+ nsCString remoteType;
+ nsCString threadName;
+ nsCString runnableName;
+ HangStack stack;
+ HangAnnotation[] annotations;
+};
+
+} // namespace mozilla
diff --git a/toolkit/components/backgroundhangmonitor/ThreadStackHelper.cpp b/toolkit/components/backgroundhangmonitor/ThreadStackHelper.cpp
new file mode 100644
index 0000000000..c43a322fd4
--- /dev/null
+++ b/toolkit/components/backgroundhangmonitor/ThreadStackHelper.cpp
@@ -0,0 +1,395 @@
+/* -*- 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 "ThreadStackHelper.h"
+#include "MainThreadUtils.h"
+#include "nsJSPrincipals.h"
+#include "nsScriptSecurityManager.h"
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#ifdef MOZ_THREADSTACKHELPER_PROFILING_STACK
+# include "js/ProfilingStack.h"
+#endif
+
+#include <utility>
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/HangTypes.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/MemoryChecking.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/UniquePtr.h"
+#include "nsThread.h"
+
+#ifdef __GNUC__
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wshadow"
+#endif
+
+#if defined(MOZ_VALGRIND)
+# include <valgrind/valgrind.h>
+#endif
+
+#include <string.h>
+#include <vector>
+#include <cstdlib>
+
+#ifdef XP_LINUX
+# include <ucontext.h>
+# include <unistd.h>
+# include <sys/syscall.h>
+#endif
+
+#ifdef __GNUC__
+# pragma GCC diagnostic pop // -Wshadow
+#endif
+
+#if defined(XP_LINUX) || defined(XP_MACOSX)
+# include <pthread.h>
+#endif
+
+#ifdef ANDROID
+# ifndef SYS_gettid
+# define SYS_gettid __NR_gettid
+# endif
+# if defined(__arm__) && !defined(__NR_rt_tgsigqueueinfo)
+// Some NDKs don't define this constant even though the kernel supports it.
+# define __NR_rt_tgsigqueueinfo (__NR_SYSCALL_BASE + 363)
+# endif
+# ifndef SYS_rt_tgsigqueueinfo
+# define SYS_rt_tgsigqueueinfo __NR_rt_tgsigqueueinfo
+# endif
+#endif
+
+namespace mozilla {
+
+// A character which we append to any string which gets truncated as a a
+// result of trying to write it into a statically allocated buffer. This just
+// makes it a little easier to know that the buffer was truncated during
+// analysis.
+const char kTruncationIndicator = '$';
+
+ThreadStackHelper::ThreadStackHelper()
+ : mStackToFill(nullptr),
+ mMaxStackSize(16),
+ mMaxBufferSize(512),
+ mDesiredStackSize(0),
+ mDesiredBufferSize(0) {
+ mThreadId = profiler_current_thread_id();
+}
+
+bool ThreadStackHelper::PrepareStackBuffer(HangStack& aStack) {
+ // If we need to grow because we used more than we could store last time,
+ // increase our maximum sizes for this time.
+ if (mDesiredBufferSize > mMaxBufferSize) {
+ mMaxBufferSize = mDesiredBufferSize;
+ }
+ if (mDesiredStackSize > mMaxStackSize) {
+ mMaxStackSize = mDesiredStackSize;
+ }
+ mDesiredBufferSize = 0;
+ mDesiredStackSize = 0;
+
+ // Clear all of the stack entries.
+ aStack.stack().ClearAndRetainStorage();
+ aStack.strbuffer().ClearAndRetainStorage();
+ aStack.modules().Clear();
+
+#ifdef MOZ_THREADSTACKHELPER_PROFILING_STACK
+ // Ensure we have enough space in our stack and string buffers for the data we
+ // want to collect.
+ if (!aStack.stack().SetCapacity(mMaxStackSize, fallible) ||
+ !aStack.strbuffer().SetCapacity(mMaxBufferSize, fallible)) {
+ return false;
+ }
+ return true;
+#else
+ return false;
+#endif
+}
+
+namespace {
+template <typename T>
+class ScopedSetPtr {
+ private:
+ T*& mPtr;
+
+ public:
+ ScopedSetPtr(T*& p, T* val) : mPtr(p) { mPtr = val; }
+ ~ScopedSetPtr() { mPtr = nullptr; }
+};
+} // namespace
+
+void ThreadStackHelper::GetStack(HangStack& aStack, nsACString& aRunnableName,
+ bool aStackWalk) {
+ aRunnableName.AssignLiteral("???");
+
+ if (!PrepareStackBuffer(aStack)) {
+ return;
+ }
+
+ Array<char, nsThread::kRunnableNameBufSize> runnableName;
+ runnableName[0] = '\0';
+
+ ScopedSetPtr<HangStack> _stackGuard(mStackToFill, &aStack);
+ ScopedSetPtr<Array<char, nsThread::kRunnableNameBufSize>> _runnableGuard(
+ mRunnableNameBuffer, &runnableName);
+
+ // XXX: We don't need to pass in ProfilerFeature::StackWalk to trigger
+ // stackwalking, as that is instead controlled by the last argument.
+ profiler_suspend_and_sample_thread(mThreadId, 0, *this, aStackWalk);
+
+ // Copy the name buffer allocation into the output string. We explicitly set
+ // the last byte to null in case we read in some corrupted data without a null
+ // terminator.
+ runnableName[nsThread::kRunnableNameBufSize - 1] = '\0';
+ aRunnableName.AssignASCII(runnableName.cbegin());
+}
+
+void ThreadStackHelper::SetIsMainThread() {
+ MOZ_RELEASE_ASSERT(mRunnableNameBuffer);
+
+ // NOTE: We cannot allocate any memory in this callback, as the target
+ // thread is suspended, so we first copy it into a stack-allocated buffer,
+ // and then once the target thread is resumed, we can copy it into a real
+ // nsCString.
+ //
+ // Currently we only store the names of runnables which are running on the
+ // main thread, so we only want to read sMainThreadRunnableName and copy its
+ // value in the case that we are currently suspending the main thread.
+ *mRunnableNameBuffer = nsThread::sMainThreadRunnableName;
+}
+
+void ThreadStackHelper::TryAppendFrame(HangEntry aFrame) {
+ MOZ_RELEASE_ASSERT(mStackToFill);
+
+ // We deduplicate identical Content, Jit, Wasm, ChromeScript and Suppressed
+ // frames.
+ switch (aFrame.type()) {
+ case HangEntry::THangEntryContent:
+ case HangEntry::THangEntryJit:
+ case HangEntry::THangEntryWasm:
+ case HangEntry::THangEntryChromeScript:
+ case HangEntry::THangEntrySuppressed:
+ if (!mStackToFill->stack().IsEmpty() &&
+ mStackToFill->stack().LastElement().type() == aFrame.type()) {
+ return;
+ }
+ break;
+ default:
+ break;
+ }
+
+ // Record that we _want_ to use another frame entry. If this exceeds
+ // mMaxStackSize, we'll allocate more room on the next hang.
+ mDesiredStackSize += 1;
+
+ // Perform the append if we have enough space to do so.
+ if (mStackToFill->stack().Capacity() > mStackToFill->stack().Length()) {
+ mStackToFill->stack().AppendElement(std::move(aFrame));
+ }
+}
+
+void ThreadStackHelper::CollectNativeLeafAddr(void* aAddr) {
+ MOZ_RELEASE_ASSERT(mStackToFill);
+ TryAppendFrame(HangEntryProgCounter(reinterpret_cast<uintptr_t>(aAddr)));
+}
+
+void ThreadStackHelper::CollectJitReturnAddr(void* aAddr) {
+ MOZ_RELEASE_ASSERT(mStackToFill);
+ TryAppendFrame(HangEntryJit());
+}
+
+void ThreadStackHelper::CollectWasmFrame(const char* aLabel) {
+ MOZ_RELEASE_ASSERT(mStackToFill);
+ // We don't want to collect WASM frames, as they are probably for content, so
+ // we just add a "(content wasm)" frame.
+ TryAppendFrame(HangEntryWasm());
+}
+
+namespace {
+
+bool IsChromeJSScript(JSScript* aScript) {
+ // May be called from another thread or inside a signal handler.
+ // We assume querying the script is safe but we must not manipulate it.
+
+ JSPrincipals* const principals = JS_GetScriptPrincipals(aScript);
+ return nsJSPrincipals::get(principals)->IsSystemPrincipal();
+}
+
+// Get the full path after the URI scheme, if the URI matches the scheme.
+// For example, GetFullPathForScheme("a://b/c/d/e", "a://") returns "b/c/d/e".
+template <size_t LEN>
+const char* GetFullPathForScheme(const char* filename,
+ const char (&scheme)[LEN]) {
+ // Account for the null terminator included in LEN.
+ if (!strncmp(filename, scheme, LEN - 1)) {
+ return filename + LEN - 1;
+ }
+ return nullptr;
+}
+
+// Get the full path after a URI component, if the URI contains the component.
+// For example, GetPathAfterComponent("a://b/c/d/e", "/c/") returns "d/e".
+template <size_t LEN>
+const char* GetPathAfterComponent(const char* filename,
+ const char (&component)[LEN]) {
+ const char* found = nullptr;
+ const char* next = strstr(filename, component);
+ while (next) {
+ // Move 'found' to end of the component, after the separator '/'.
+ // 'LEN - 1' accounts for the null terminator included in LEN,
+ found = next + LEN - 1;
+ // Resume searching before the separator '/'.
+ next = strstr(found - 1, component);
+ }
+ return found;
+}
+
+} // namespace
+
+bool ThreadStackHelper::MaybeAppendDynamicStackFrame(Span<const char> aBuf) {
+ mDesiredBufferSize += aBuf.Length() + 1;
+
+ if (mStackToFill->stack().Capacity() > mStackToFill->stack().Length() &&
+ (mStackToFill->strbuffer().Capacity() -
+ mStackToFill->strbuffer().Length()) > aBuf.Length() + 1) {
+ // NOTE: We only increment this if we're going to successfully append.
+ mDesiredStackSize += 1;
+ uint32_t start = mStackToFill->strbuffer().Length();
+ mStackToFill->strbuffer().AppendElements(aBuf.Elements(), aBuf.Length());
+ mStackToFill->strbuffer().AppendElement('\0');
+ mStackToFill->stack().AppendElement(HangEntryBufOffset(start));
+ return true;
+ }
+ return false;
+}
+
+void ThreadStackHelper::CollectProfilingStackFrame(
+ const js::ProfilingStackFrame& aFrame) {
+ // For non-js frames, first try to get the dynamic string and fit it in,
+ // otherwise just get the label.
+ if (!aFrame.isJsFrame()) {
+ const char* frameLabel = aFrame.label();
+ if (aFrame.isNonsensitive() && aFrame.dynamicString()) {
+ const char* dynamicString = aFrame.dynamicString();
+ char buffer[128];
+ size_t len = SprintfLiteral(buffer, "%s %s", frameLabel, dynamicString);
+ if (len > sizeof(buffer)) {
+ buffer[sizeof(buffer) - 1] = kTruncationIndicator;
+ len = sizeof(buffer);
+ }
+ if (MaybeAppendDynamicStackFrame(Span(buffer, len))) {
+ return;
+ }
+ }
+
+ // frameLabel is a statically allocated string, so we want to store a
+ // reference to it without performing any allocations. This is important, as
+ // we aren't allowed to allocate within this function.
+ //
+ // The variant for this kind of label in our HangStack object is a
+ // `nsCString`, which normally contains heap allocated string data. However,
+ // `nsCString` has an optimization for literal strings which causes the
+ // backing data to not be copied when being copied between nsCString
+ // objects.
+ //
+ // We take advantage of that optimization by creating a nsCString object
+ // which has the LITERAL flag set. Without this optimization, this code
+ // would be incorrect.
+ nsCString label;
+ label.AssignLiteral(frameLabel, strlen(frameLabel));
+
+ // Let's make sure we don't deadlock here, by asserting that `label`'s
+ // backing data matches.
+ MOZ_RELEASE_ASSERT(label.BeginReading() == frameLabel,
+ "String copy performed during "
+ "ThreadStackHelper::CollectProfilingStackFrame");
+ TryAppendFrame(label);
+ return;
+ }
+
+ if (!aFrame.script()) {
+ TryAppendFrame(HangEntrySuppressed());
+ return;
+ }
+
+ if (!IsChromeJSScript(aFrame.script())) {
+ TryAppendFrame(HangEntryContent());
+ return;
+ }
+
+ // Rather than using the profiler's dynamic string, we compute our own string.
+ // This is because we want to do some size-saving strategies, and throw out
+ // information which won't help us as much.
+ const char* filename = JS_GetScriptFilename(aFrame.script());
+
+ char buffer[256]; // Should be enough to fit our longest js function and file
+ // names.
+ size_t len = 0;
+ if (JSFunction* func = aFrame.function()) {
+ if (JSString* str = JS_GetMaybePartialFunctionDisplayId(func)) {
+ JSLinearString* linear = JS_ASSERT_STRING_IS_LINEAR(str);
+ len = JS::GetLinearStringLength(linear);
+ JS::LossyCopyLinearStringChars(buffer, linear,
+ std::min(len, sizeof(buffer)));
+ // NOTE: >= so that we account for the trailing space that we'd want to
+ // otherwise append.
+ if (len >= sizeof(buffer)) {
+ len = sizeof(buffer);
+ buffer[sizeof(buffer) - 1] = kTruncationIndicator;
+ } else {
+ buffer[len++] = ' ';
+ }
+ }
+ }
+
+ unsigned lineno = JS_PCToLineNumber(aFrame.script(), aFrame.pc());
+
+ // Some script names are in the form "foo -> bar -> baz".
+ // Here we find the origin of these redirected scripts.
+ const char* basename = GetPathAfterComponent(filename, " -> ");
+ if (basename) {
+ filename = basename;
+ }
+
+ // Strip chrome:// or resource:// off of the filename if present.
+ basename = GetFullPathForScheme(filename, "chrome://");
+ if (!basename) {
+ basename = GetFullPathForScheme(filename, "resource://");
+ }
+ if (!basename) {
+ // If we're in an add-on script, under the {profile}/extensions
+ // directory, extract the path after the /extensions/ part.
+ basename = GetPathAfterComponent(filename, "/extensions/");
+ }
+ if (!basename) {
+ // Only keep the file base name for paths outside the above formats.
+ basename = strrchr(filename, '/');
+ basename = basename ? basename + 1 : filename;
+ // Look for Windows path separator as well.
+ filename = strrchr(basename, '\\');
+ if (filename) {
+ basename = filename + 1;
+ }
+ }
+
+ len +=
+ SprintfBuf(buffer + len, sizeof(buffer) - len, "%s:%u", basename, lineno);
+ if (len > sizeof(buffer)) {
+ buffer[sizeof(buffer) - 1] = kTruncationIndicator;
+ len = sizeof(buffer);
+ }
+ if (MaybeAppendDynamicStackFrame(Span(buffer, len))) {
+ return;
+ }
+
+ TryAppendFrame(HangEntryChromeScript());
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/backgroundhangmonitor/ThreadStackHelper.h b/toolkit/components/backgroundhangmonitor/ThreadStackHelper.h
new file mode 100644
index 0000000000..e54078d2dd
--- /dev/null
+++ b/toolkit/components/backgroundhangmonitor/ThreadStackHelper.h
@@ -0,0 +1,111 @@
+/* -*- 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 mozilla_ThreadStackHelper_h
+#define mozilla_ThreadStackHelper_h
+
+#ifdef MOZ_GECKO_PROFILER
+
+# include "js/ProfilingStack.h"
+# include "GeckoProfiler.h"
+# include "HangDetails.h"
+# include "mozilla/Span.h"
+# include "nsThread.h"
+
+# include <stddef.h>
+
+# if defined(XP_LINUX)
+# include <signal.h>
+# include <semaphore.h>
+# include <sys/types.h>
+# elif defined(XP_WIN)
+# include <windows.h>
+# elif defined(XP_MACOSX)
+# include <mach/mach.h>
+# endif
+
+// Support profiling stack and native stack on these platforms.
+# if defined(XP_LINUX) || defined(XP_WIN) || defined(XP_MACOSX)
+# define MOZ_THREADSTACKHELPER_PROFILING_STACK
+# define MOZ_THREADSTACKHELPER_NATIVE_STACK
+# endif
+
+// Android x86 builds consistently crash in the Background Hang Reporter. bug
+// 1368520.
+# if defined(__ANDROID__)
+# undef MOZ_THREADSTACKHELPER_PROFILING_STACK
+# undef MOZ_THREADSTACKHELPER_NATIVE_STACK
+# endif
+
+namespace mozilla {
+
+/**
+ * ThreadStackHelper is used to retrieve the profiler's "profiling stack" of a
+ * thread, as an alternative of using the profiler to take a profile.
+ * The target thread first declares an ThreadStackHelper instance;
+ * then another thread can call ThreadStackHelper::GetStack to retrieve
+ * the profiling stack of the target thread at that instant.
+ *
+ * Only non-copying labels are included in the stack, which means labels
+ * with custom text and markers are not included.
+ */
+class ThreadStackHelper : public ProfilerStackCollector {
+ private:
+ HangStack* mStackToFill;
+ Array<char, nsThread::kRunnableNameBufSize>* mRunnableNameBuffer;
+ size_t mMaxStackSize;
+ size_t mMaxBufferSize;
+ size_t mDesiredStackSize;
+ size_t mDesiredBufferSize;
+
+ bool PrepareStackBuffer(HangStack& aStack);
+
+ public:
+ /**
+ * Create a ThreadStackHelper instance targeting the current thread.
+ */
+ ThreadStackHelper();
+
+ /**
+ * Retrieve the current interleaved stack of the thread associated with this
+ * ThreadStackHelper.
+ *
+ * @param aStack HangStack instance to be filled.
+ * @param aRunnableName The name of the current runnable on the target thread.
+ * @param aStackWalk If true, native stack frames will be collected
+ * along with profiling stack frames.
+ */
+ void GetStack(HangStack& aStack, nsACString& aRunnableName, bool aStackWalk);
+
+ /**
+ * Retrieve the thread's profiler thread ID.
+ */
+ ProfilerThreadId GetThreadId() const { return mThreadId; }
+
+ protected:
+ /**
+ * ProfilerStackCollector
+ */
+ virtual void SetIsMainThread() override;
+ virtual void CollectNativeLeafAddr(void* aAddr) override;
+ virtual void CollectJitReturnAddr(void* aAddr) override;
+ virtual void CollectWasmFrame(const char* aLabel) override;
+ virtual void CollectProfilingStackFrame(
+ const js::ProfilingStackFrame& aEntry) override;
+
+ private:
+ bool MaybeAppendDynamicStackFrame(mozilla::Span<const char> aBuf);
+ void TryAppendFrame(mozilla::HangEntry aFrame);
+
+ // The profiler's unique thread identifier for the target thread.
+ ProfilerThreadId mThreadId;
+};
+
+} // namespace mozilla
+
+#endif // MOZ_GECKO_PROFILER
+
+#endif // mozilla_ThreadStackHelper_h
diff --git a/toolkit/components/backgroundhangmonitor/components.conf b/toolkit/components/backgroundhangmonitor/components.conf
new file mode 100644
index 0000000000..ba052e29f3
--- /dev/null
+++ b/toolkit/components/backgroundhangmonitor/components.conf
@@ -0,0 +1,16 @@
+# -*- 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 = [
+ {
+ 'cid': '{117c8cdf-69e6-4f31-a439-b8a654c67127}',
+ 'contract_ids': ['@mozilla.org/bhr-telemetry-service;1'],
+ 'esModule': 'resource://gre/modules/BHRTelemetryService.sys.mjs',
+ 'constructor': 'BHRTelemetryService',
+ 'categories': {'profile-after-change': 'BHRTelemetryService'},
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+]
diff --git a/toolkit/components/backgroundhangmonitor/moz.build b/toolkit/components/backgroundhangmonitor/moz.build
new file mode 100644
index 0000000000..78cc8065b6
--- /dev/null
+++ b/toolkit/components/backgroundhangmonitor/moz.build
@@ -0,0 +1,68 @@
+# -*- 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/.
+
+# NOTE: These config options must match the ones in both android/'s and
+# browser/'s package-manifest.in.
+
+# BHR disabled outside of Nightly builds due to expected high ping frequency.
+# BHR disabled for Release builds because of bug 965392.
+# BHR disabled for debug builds because of bug 979069.
+# BHR disabled for TSan builds because of bug 1121216.
+# BHR disabled for ASan builds because of bug 1445441.
+# When changing these conditions, please also change the matching conditions in
+# tools/profiler/public/ProfilerLabels.h and xpcom/threads/moz.build.
+if (
+ CONFIG["NIGHTLY_BUILD"]
+ and not CONFIG["MOZ_DEBUG"]
+ and not CONFIG["MOZ_TSAN"]
+ and not CONFIG["MOZ_ASAN"]
+):
+ DEFINES["MOZ_ENABLE_BACKGROUND_HANG_MONITOR"] = 1
+
+ EXTRA_JS_MODULES += [
+ "BHRTelemetryService.sys.mjs",
+ ]
+
+ XPCOM_MANIFESTS += [
+ "components.conf",
+ ]
+
+ XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell.toml"]
+
+XPIDL_SOURCES += [
+ "nsIHangDetails.idl",
+]
+
+XPIDL_MODULE = "backgroundhangmonitor"
+
+EXPORTS.mozilla += [
+ "BackgroundHangMonitor.h",
+ "HangAnnotations.h",
+ "HangDetails.h",
+]
+
+UNIFIED_SOURCES += [
+ "BackgroundHangMonitor.cpp",
+ "HangAnnotations.cpp",
+ "HangDetails.cpp",
+]
+
+IPDL_SOURCES += [
+ "HangTypes.ipdlh",
+]
+
+if CONFIG["MOZ_GECKO_PROFILER"]:
+ UNIFIED_SOURCES += [
+ "ThreadStackHelper.cpp",
+ ]
+
+LOCAL_INCLUDES += [
+ "/caps", # For nsScriptSecurityManager.h
+]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/toolkit/components/backgroundhangmonitor/nsIHangDetails.idl b/toolkit/components/backgroundhangmonitor/nsIHangDetails.idl
new file mode 100644
index 0000000000..f9c6ba2de0
--- /dev/null
+++ b/toolkit/components/backgroundhangmonitor/nsIHangDetails.idl
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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++
+namespace mozilla {
+class HangDetails;
+}
+%}
+
+[ref] native HangDetailsRef(mozilla::HangDetails);
+
+/**
+ * A scriptable interface for getting information about a BHR detected hang.
+ * This is the type of the subject of the "bhr-thread-hang" observer topic.
+ */
+[scriptable, uuid(23d63fff-38d6-4003-9c57-2c90aca1180a)]
+interface nsIHangDetails : nsISupports
+{
+ /**
+ * The hang was persisted to disk as a permahang, so we can clear the
+ * permahang file once we submit this.
+ */
+ readonly attribute bool wasPersisted;
+
+ /**
+ * The detected duration of the hang in milliseconds.
+ */
+ readonly attribute double duration;
+
+ /**
+ * The name of the thread which hung.
+ */
+ readonly attribute ACString thread;
+
+ /**
+ * The name of the runnable which hung if it hung on the main thread.
+ */
+ readonly attribute ACString runnableName;
+
+ /**
+ * The type of process which produced the hang. This should be either:
+ * "default", "content", or "gpu".
+ */
+ readonly attribute ACString process;
+
+ /**
+ * The remote type of the content process which produced the hang.
+ */
+ readonly attribute AUTF8String remoteType;
+
+ /**
+ * Returns the stack which was captured by BHR. The offset is encoded as a hex
+ * string, as it can contain numbers larger than JS can hold losslessly.
+ *
+ * This value takes the following form:
+ * [ [moduleIndex, offset], ... ]
+ */
+ [implicit_jscontext] readonly attribute jsval stack;
+
+ /**
+ * Returns the modules which were captured by BHR.
+ *
+ * This value takes the following form:
+ * [ ["fileName", "BreakpadId"], ... ]
+ */
+ [implicit_jscontext] readonly attribute jsval modules;
+
+ /**
+ * The hang annotations which were captured when the hang occured. This
+ * attribute is a JS object of key-value pairs.
+ */
+ [implicit_jscontext] readonly attribute jsval annotations;
+};
diff --git a/toolkit/components/backgroundhangmonitor/tests/child_cause_hang.js b/toolkit/components/backgroundhangmonitor/tests/child_cause_hang.js
new file mode 100644
index 0000000000..adf96170b6
--- /dev/null
+++ b/toolkit/components/backgroundhangmonitor/tests/child_cause_hang.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function ensureProfilerInitialized() {
+ // Starting and stopping the profiler with the "stackwalk" flag will cause the
+ // profiler's stackwalking features to be synchronously initialized. This
+ // should prevent us from not initializing BHR quickly enough.
+ let features = ["stackwalk"];
+ Services.profiler.StartProfiler(1000, 10, features);
+ Services.profiler.StopProfiler();
+}
+
+add_task(async function childCauseHang() {
+ ensureProfilerInitialized();
+
+ executeSoon(() => {
+ let startTime = Date.now();
+ // eslint-disable-next-line no-empty
+ while (Date.now() - startTime < 2000) {}
+ });
+
+ await do_await_remote_message("bhr_hangs_detected");
+});
diff --git a/toolkit/components/backgroundhangmonitor/tests/test_BHRObserver.js b/toolkit/components/backgroundhangmonitor/tests/test_BHRObserver.js
new file mode 100644
index 0000000000..cf6d6633b8
--- /dev/null
+++ b/toolkit/components/backgroundhangmonitor/tests/test_BHRObserver.js
@@ -0,0 +1,164 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+
+function ensureProfilerInitialized() {
+ // Starting and stopping the profiler with the "stackwalk" flag will cause the
+ // profiler's stackwalking features to be synchronously initialized. This
+ // should prevent us from not initializing BHR quickly enough.
+ let features = ["stackwalk"];
+ Services.profiler.StartProfiler(1000, 10, features);
+ Services.profiler.StopProfiler();
+}
+
+add_task(async function test_BHRObserver() {
+ if (!Services.telemetry.canRecordExtended) {
+ ok("Hang reporting not enabled.");
+ return;
+ }
+
+ ensureProfilerInitialized();
+
+ let telSvc =
+ Cc["@mozilla.org/bhr-telemetry-service;1"].getService().wrappedJSObject;
+ ok(telSvc, "Should have BHRTelemetryService");
+ let beforeLen = telSvc.payload.hangs.length;
+
+ if (Services.appinfo.OS === "Linux" || Services.appinfo.OS === "Android") {
+ // We use the rt_tgsigqueueinfo syscall on Linux which requires a
+ // certain kernel version. It's not an error if the system running
+ // the test is older than that.
+ let kernel =
+ Services.sysinfo.get("kernel_version") || Services.sysinfo.get("version");
+ if (Services.vc.compare(kernel, "2.6.31") < 0) {
+ ok("Hang reporting not supported for old kernel.");
+ return;
+ }
+ }
+
+ let hangsPromise = new Promise(resolve => {
+ let hangs = [];
+ const onThreadHang = subject => {
+ let hang = subject.QueryInterface(Ci.nsIHangDetails);
+ if (hang.thread.startsWith("Gecko")) {
+ hangs.push(hang);
+ if (hangs.length >= 3) {
+ Services.obs.removeObserver(onThreadHang, "bhr-thread-hang");
+ resolve(hangs);
+ }
+ }
+ };
+ Services.obs.addObserver(onThreadHang, "bhr-thread-hang");
+ });
+
+ // We're going to trigger two hangs, of various lengths. One should be a
+ // transient hang, and the other a permanent hang. We'll wait for the hangs to
+ // be recorded.
+
+ executeSoon(() => {
+ let startTime = Date.now();
+ // eslint-disable-next-line no-empty
+ while (Date.now() - startTime < 10000) {}
+ });
+
+ executeSoon(() => {
+ let startTime = Date.now();
+ // eslint-disable-next-line no-empty
+ while (Date.now() - startTime < 1000) {}
+ });
+
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.OverridePreRelease,
+ true
+ );
+ let childDone = run_test_in_child("child_cause_hang.js");
+
+ // Now we wait for the hangs to have their bhr-thread-hang message fired for
+ // them, collect them, and analyize the response.
+ let hangs = await hangsPromise;
+ equal(hangs.length, 3);
+ hangs.forEach(hang => {
+ Assert.greater(hang.duration, 0);
+ ok(hang.thread == "Gecko" || hang.thread == "Gecko_Child");
+ equal(typeof hang.runnableName, "string");
+
+ // hang.stack
+ ok(Array.isArray(hang.stack));
+ ok(!!hang.stack.length);
+ hang.stack.forEach(entry => {
+ // Each stack frame entry is either a native or pseudostack entry. A
+ // native stack entry is an array with module index (number), and offset
+ // (hex string), while the pseudostack entry is a bare string.
+ if (Array.isArray(entry)) {
+ equal(entry.length, 2);
+ equal(typeof entry[0], "number");
+ equal(typeof entry[1], "string");
+ } else {
+ equal(typeof entry, "string");
+ }
+ });
+
+ // hang.modules
+ ok(Array.isArray(hang.modules));
+ hang.modules.forEach(module => {
+ ok(Array.isArray(module));
+ equal(module.length, 2);
+ equal(typeof module[0], "string");
+ equal(typeof module[1], "string");
+ });
+
+ // hang.annotations
+ ok(Array.isArray(hang.annotations));
+ hang.annotations.forEach(annotation => {
+ ok(Array.isArray(annotation));
+ equal(annotation.length, 2);
+ equal(typeof annotation[0], "string");
+ equal(typeof annotation[1], "string");
+ });
+ });
+
+ // Check that the telemetry service collected pings which make sense
+ Assert.greaterOrEqual(telSvc.payload.hangs.length - beforeLen, 3);
+ ok(Array.isArray(telSvc.payload.modules));
+ telSvc.payload.modules.forEach(module => {
+ ok(Array.isArray(module));
+ equal(module.length, 2);
+ equal(typeof module[0], "string");
+ equal(typeof module[1], "string");
+ });
+
+ telSvc.payload.hangs.forEach(hang => {
+ Assert.greater(hang.duration, 0);
+ ok(hang.thread == "Gecko" || hang.thread == "Gecko_Child");
+ equal(typeof hang.runnableName, "string");
+
+ // hang.stack
+ ok(Array.isArray(hang.stack));
+ ok(!!hang.stack.length);
+ hang.stack.forEach(entry => {
+ // Each stack frame entry is either a native or pseudostack entry. A
+ // native stack entry is an array with module index (number), and offset
+ // (hex string), while the pseudostack entry is a bare string.
+ if (Array.isArray(entry)) {
+ equal(entry.length, 2);
+ equal(typeof entry[0], "number");
+ Assert.less(entry[0], telSvc.payload.modules.length);
+ equal(typeof entry[1], "string");
+ } else {
+ equal(typeof entry, "string");
+ }
+ });
+
+ // hang.annotations
+ equal(typeof hang.annotations, "object");
+ Object.keys(hang.annotations).forEach(key => {
+ equal(typeof hang.annotations[key], "string");
+ });
+ });
+
+ do_send_remote_message("bhr_hangs_detected");
+ await childDone;
+});
diff --git a/toolkit/components/backgroundhangmonitor/tests/xpcshell.toml b/toolkit/components/backgroundhangmonitor/tests/xpcshell.toml
new file mode 100644
index 0000000000..d0ea28c83c
--- /dev/null
+++ b/toolkit/components/backgroundhangmonitor/tests/xpcshell.toml
@@ -0,0 +1,14 @@
+[DEFAULT]
+
+["test_BHRObserver.js"]
+# BHR is disabled on android and outside of nightly
+skip-if = [
+ "debug",
+ "os == 'android'",
+ "release_or_beta",
+ "os == 'mac'", # Bug 1417723
+ "win11_2009 && bits == 32", # Bug 1760134
+ "os == 'win' && msix",
+]
+support-files = ["child_cause_hang.js"]
+run-sequentially = "very high failure rate in parallel"