Backed out changeset bd63a8fecf00 (bug 1380081)
authorSebastian Hengst <archaeopteryx@coole-files.de>
Fri, 11 Aug 2017 09:28:47 +0200
changeset 374268 f6b47124336fba85e8ba753ae607c2347b5665e6
parent 374267 36c4ddeb51a14c5e4046376dd22501fa22d9889f
child 374269 e6d57bb2b33e6b37585b44e0bfd3f76d45f808d2
push id32318
push userkwierso@gmail.com
push dateFri, 11 Aug 2017 20:16:01 +0000
treeherdermozilla-central@80ff3f300e05 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1380081
milestone57.0a1
backs outbd63a8fecf00a2f185e8e87dc8e8e566e24faf9c
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out changeset bd63a8fecf00 (bug 1380081)
toolkit/components/telemetry/Telemetry.cpp
toolkit/components/telemetry/Telemetry.h
toolkit/components/telemetry/TelemetrySession.jsm
toolkit/components/telemetry/ThreadHangStats.cpp
toolkit/components/telemetry/ThreadHangStats.h
toolkit/components/telemetry/docs/data/main-ping.rst
toolkit/components/telemetry/nsITelemetry.idl
toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
xpcom/threads/BackgroundHangMonitor.cpp
xpcom/threads/BackgroundHangMonitor.h
--- a/toolkit/components/telemetry/Telemetry.cpp
+++ b/toolkit/components/telemetry/Telemetry.cpp
@@ -143,16 +143,17 @@ public:
                                Telemetry::ProcessedStack &aStack,
                                int32_t aSystemUptime,
                                int32_t aFirefoxUptime,
                                HangAnnotationsPtr aAnnotations);
 #endif
 #if defined(MOZ_GECKO_PROFILER)
   static void DoStackCapture(const nsACString& aKey);
 #endif
+  static void RecordThreadHangStats(Telemetry::ThreadHangStats&& aStats);
   size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
   struct Stat {
     uint32_t hitCount;
     uint32_t totalTime;
   };
   struct StmtStats {
     struct Stat mainThread;
     struct Stat otherThreads;
@@ -197,16 +198,20 @@ private:
   Atomic<bool> mCanRecordBase;
   Atomic<bool> mCanRecordExtended;
 
 #if defined(MOZ_GECKO_PROFILER)
   // Stores data about stacks captured on demand.
   KeyedStackCapturer mStackCapturer;
 #endif
 
+  // mThreadHangStats stores recorded, inactive thread hang stats
+  Vector<Telemetry::ThreadHangStats> mThreadHangStats;
+  Mutex mThreadHangStatsMutex;
+
   CombinedStacks mLateWritesStacks; // This is collected out of the main thread.
   bool mCachedTelemetryData;
   uint32_t mLastShutdownTime;
   uint32_t mFailedLockCount;
   nsCOMArray<nsIFetchTelemetryDataCallback> mCallbacks;
   friend class nsFetchTelemetryData;
 
   WebrtcTelemetry mWebrtcTelemetry;
@@ -475,32 +480,34 @@ TelemetryImpl::AsyncFetchTelemetryData(n
   return NS_OK;
 }
 
 TelemetryImpl::TelemetryImpl()
   : mHashMutex("Telemetry::mHashMutex")
   , mHangReportsMutex("Telemetry::mHangReportsMutex")
   , mCanRecordBase(false)
   , mCanRecordExtended(false)
+  , mThreadHangStatsMutex("Telemetry::mThreadHangStatsMutex")
   , mCachedTelemetryData(false)
   , mLastShutdownTime(0)
   , mFailedLockCount(0)
 {
   // We expect TelemetryHistogram::InitializeGlobalState() to have been
   // called before we get to this point.
   MOZ_ASSERT(TelemetryHistogram::GlobalStateHasBeenInitialized());
 }
 
 TelemetryImpl::~TelemetryImpl() {
   UnregisterWeakMemoryReporter(this);
 
   // This is still racey as access to these collections is guarded using sTelemetry.
   // We will fix this in bug 1367344.
   MutexAutoLock hashLock(mHashMutex);
   MutexAutoLock hangReportsLock(mHangReportsMutex);
+  MutexAutoLock threadHangsLock(mThreadHangStatsMutex);
 }
 
 void
 TelemetryImpl::InitMemoryReporter() {
   RegisterWeakMemoryReporter(this);
 }
 
 bool
@@ -1053,16 +1060,53 @@ ReadStack(const char *aFileName, Telemet
       index
     };
     stack.AddFrame(frame);
   }
 
   aStack = stack;
 }
 
+NS_IMETHODIMP
+TelemetryImpl::GetThreadHangStats(JSContext* cx, JS::MutableHandle<JS::Value> ret)
+{
+  JS::RootedObject retObj(cx, JS_NewArrayObject(cx, 0));
+  if (!retObj) {
+    return NS_ERROR_FAILURE;
+  }
+  size_t threadIndex = 0;
+
+  if (!BackgroundHangMonitor::IsDisabled()) {
+    /* First add active threads; we need to hold |iter| (and its lock)
+       throughout this method to avoid a race condition where a thread can
+       be recorded twice if the thread is destroyed while this method is
+       running */
+    BackgroundHangMonitor::ThreadHangStatsIterator iter;
+    for (Telemetry::ThreadHangStats* histogram = iter.GetNext();
+         histogram; histogram = iter.GetNext()) {
+      JS::RootedObject obj(cx, CreateJSThreadHangStats(cx, *histogram));
+      if (!JS_DefineElement(cx, retObj, threadIndex++, obj, JSPROP_ENUMERATE)) {
+        return NS_ERROR_FAILURE;
+      }
+    }
+  }
+
+  // Add saved threads next
+  MutexAutoLock autoLock(mThreadHangStatsMutex);
+  for (auto & stat : mThreadHangStats) {
+    JS::RootedObject obj(cx,
+      CreateJSThreadHangStats(cx, stat));
+    if (!JS_DefineElement(cx, retObj, threadIndex++, obj, JSPROP_ENUMERATE)) {
+      return NS_ERROR_FAILURE;
+    }
+  }
+  ret.setObject(*retObj);
+  return NS_OK;
+}
+
 void
 TelemetryImpl::ReadLateWritesStacks(nsIFile* aProfileDir)
 {
   nsAutoCString nativePath;
   nsresult rv = aProfileDir->GetNativePath(nativePath);
   if (NS_FAILED(rv)) {
     return;
   }
@@ -1581,16 +1625,28 @@ TelemetryImpl::DoStackCapture(const nsAC
 nsresult
 TelemetryImpl::CaptureStack(const nsACString& aKey) {
 #if defined(MOZ_GECKO_PROFILER)
   TelemetryImpl::DoStackCapture(aKey);
 #endif
   return NS_OK;
 }
 
+void
+TelemetryImpl::RecordThreadHangStats(Telemetry::ThreadHangStats&& aStats)
+{
+  if (!sTelemetry || !TelemetryHistogram::CanRecordExtended())
+    return;
+
+  MutexAutoLock autoLock(sTelemetry->mThreadHangStatsMutex);
+
+  // Ignore OOM.
+  mozilla::Unused << sTelemetry->mThreadHangStats.append(Move(aStats));
+}
+
 bool
 TelemetryImpl::CanRecordBase()
 {
   if (!sTelemetry) {
     return false;
   }
   bool canRecordBase;
   nsresult rv = sTelemetry->GetCanRecordBase(&canRecordBase);
@@ -1798,16 +1854,20 @@ TelemetryImpl::SizeOfIncludingThis(mozil
     MutexAutoLock lock(mHashMutex);
     n += mPrivateSQL.SizeOfExcludingThis(aMallocSizeOf);
     n += mSanitizedSQL.SizeOfExcludingThis(aMallocSizeOf);
   }
   { // Scope for mHangReportsMutex lock
     MutexAutoLock lock(mHangReportsMutex);
     n += mHangReports.SizeOfExcludingThis(aMallocSizeOf);
   }
+  { // Scope for mThreadHangStatsMutex lock
+    MutexAutoLock lock(mThreadHangStatsMutex);
+    n += mThreadHangStats.sizeOfExcludingThis(aMallocSizeOf);
+  }
 
   // It's a bit gross that we measure this other stuff that lives outside of
   // TelemetryImpl... oh well.
   if (sTelemetryIOObserver) {
     n += sTelemetryIOObserver->SizeOfIncludingThis(aMallocSizeOf);
   }
 
   n += TelemetryHistogram::GetHistogramSizesofIncludingThis(aMallocSizeOf);
@@ -2024,16 +2084,22 @@ void RecordChromeHang(uint32_t duration,
 void CaptureStack(const nsACString& aKey)
 {
 #if defined(MOZ_GECKO_PROFILER)
   TelemetryImpl::DoStackCapture(aKey);
 #endif
 }
 #endif
 
+void RecordThreadHangStats(ThreadHangStats&& aStats)
+{
+  TelemetryImpl::RecordThreadHangStats(Move(aStats));
+}
+
+
 void
 WriteFailedProfileLock(nsIFile* aProfileDir)
 {
   nsCOMPtr<nsIFile> file;
   nsresult rv = GetFailedProfileLockFile(getter_AddRefs(file), aProfileDir);
   NS_ENSURE_SUCCESS_VOID(rv);
   int64_t fileSize = 0;
   rv = file->GetFileSize(&fileSize);
--- a/toolkit/components/telemetry/Telemetry.h
+++ b/toolkit/components/telemetry/Telemetry.h
@@ -336,16 +336,30 @@ void RecordChromeHang(uint32_t aDuration
  *
  * @param aKey - A user defined key associated with the captured stack.
  *
  * NOTE: Unwinding call stacks is an expensive operation performance-wise.
  */
 void CaptureStack(const nsCString& aKey);
 #endif
 
+class ThreadHangStats;
+
+/**
+ * Move a ThreadHangStats to Telemetry storage. Normally Telemetry queries
+ * for active ThreadHangStats through BackgroundHangMonitor, but once a
+ * thread exits, the thread's copy of ThreadHangStats needs to be moved to
+ * inside Telemetry using this function.
+ *
+ * @param aStats ThreadHangStats to save; the data inside aStats
+ *               will be moved and aStats should be treated as
+ *               invalid after this function returns
+ */
+void RecordThreadHangStats(ThreadHangStats&& aStats);
+
 /**
  * Record a failed attempt at locking the user's profile.
  *
  * @param aProfileDir The profile directory whose lock attempt failed
  */
 void WriteFailedProfileLock(nsIFile* aProfileDir);
 
 /**
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -972,16 +972,26 @@ var Impl = {
       for (let proc of Object.keys(snapshot)) {
         snapshot[proc] = snapshot[proc].filter(e => !e[1].startsWith("telemetry.test"));
       }
     }
 
     return snapshot;
   },
 
+  getThreadHangStats: function getThreadHangStats(stats) {
+    stats.forEach((thread) => {
+      thread.activity = this.packHistogram(thread.activity);
+      thread.hangs.forEach((hang) => {
+        hang.histogram = this.packHistogram(hang.histogram);
+      });
+    });
+    return stats;
+  },
+
   /**
    * Descriptive metadata
    *
    * @param  reason
    *         The reason for the telemetry ping, this will be included in the
    *         returned metadata,
    * @return The metadata as a JS object
    */
@@ -1225,16 +1235,17 @@ var Impl = {
     let payloadObj = {
       ver: PAYLOAD_VERSION,
       simpleMeasurements,
     };
 
     // Add extended set measurements common to chrome & content processes
     if (Telemetry.canRecordExtended) {
       payloadObj.chromeHangs = protect(() => Telemetry.chromeHangs);
+      payloadObj.threadHangStats = protect(() => this.getThreadHangStats(Telemetry.threadHangStats));
       payloadObj.log = protect(() => TelemetryLog.entries());
       payloadObj.webrtc = protect(() => Telemetry.webrtcStats);
     }
 
     if (Utils.isContentProcess) {
       return payloadObj;
     }
 
--- a/toolkit/components/telemetry/ThreadHangStats.cpp
+++ b/toolkit/components/telemetry/ThreadHangStats.cpp
@@ -4,19 +4,252 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/HangAnnotations.h"
 #include "ThreadHangStats.h"
 #include "nsITelemetry.h"
 #include "HangReports.h"
 #include "jsapi.h"
 
+namespace {
+
+using namespace mozilla;
+using namespace mozilla::HangMonitor;
+using namespace mozilla::Telemetry;
+
+static JSObject*
+CreateJSTimeHistogram(JSContext* cx, const Telemetry::TimeHistogram& time)
+{
+  /* Create JS representation of TimeHistogram,
+     in the format of Chromium-style histograms. */
+  JS::RootedObject ret(cx, JS_NewPlainObject(cx));
+  if (!ret) {
+    return nullptr;
+  }
+
+  if (!JS_DefineProperty(cx, ret, "min", time.GetBucketMin(0),
+                         JSPROP_ENUMERATE) ||
+      !JS_DefineProperty(cx, ret, "max",
+                         time.GetBucketMax(ArrayLength(time) - 1),
+                         JSPROP_ENUMERATE) ||
+      !JS_DefineProperty(cx, ret, "histogram_type",
+                         nsITelemetry::HISTOGRAM_EXPONENTIAL,
+                         JSPROP_ENUMERATE)) {
+    return nullptr;
+  }
+  // TODO: calculate "sum"
+  if (!JS_DefineProperty(cx, ret, "sum", 0, JSPROP_ENUMERATE)) {
+    return nullptr;
+  }
+
+  JS::RootedObject ranges(
+    cx, JS_NewArrayObject(cx, ArrayLength(time) + 1));
+  JS::RootedObject counts(
+    cx, JS_NewArrayObject(cx, ArrayLength(time) + 1));
+  if (!ranges || !counts) {
+    return nullptr;
+  }
+  /* In a Chromium-style histogram, the first bucket is an "under" bucket
+     that represents all values below the histogram's range. */
+  if (!JS_DefineElement(cx, ranges, 0, time.GetBucketMin(0), JSPROP_ENUMERATE) ||
+      !JS_DefineElement(cx, counts, 0, 0, JSPROP_ENUMERATE)) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < ArrayLength(time); i++) {
+    if (!JS_DefineElement(cx, ranges, i + 1, time.GetBucketMax(i),
+                          JSPROP_ENUMERATE) ||
+        !JS_DefineElement(cx, counts, i + 1, time[i], JSPROP_ENUMERATE)) {
+      return nullptr;
+    }
+  }
+  if (!JS_DefineProperty(cx, ret, "ranges", ranges, JSPROP_ENUMERATE) ||
+      !JS_DefineProperty(cx, ret, "counts", counts, JSPROP_ENUMERATE)) {
+    return nullptr;
+  }
+  return ret;
+}
+
+static JSObject*
+CreateJSHangStack(JSContext* cx, const Telemetry::HangStack& stack)
+{
+  JS::RootedObject ret(cx, JS_NewArrayObject(cx, stack.length()));
+  if (!ret) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < stack.length(); i++) {
+    JS::RootedString string(cx, JS_NewStringCopyZ(cx, stack[i]));
+    if (!JS_DefineElement(cx, ret, i, string, JSPROP_ENUMERATE)) {
+      return nullptr;
+    }
+  }
+  return ret;
+}
+
+static void
+CreateJSHangAnnotations(JSContext* cx, const HangAnnotationsVector& annotations,
+                        JS::MutableHandleObject returnedObject)
+{
+  JS::RootedObject annotationsArray(cx, JS_NewArrayObject(cx, 0));
+  if (!annotationsArray) {
+    returnedObject.set(nullptr);
+    return;
+  }
+  // We keep track of the annotations we reported in this hash set, so we can
+  // discard duplicated ones.
+  nsTHashtable<nsStringHashKey> reportedAnnotations;
+  size_t annotationIndex = 0;
+  for (const auto & curAnnotations : annotations) {
+    JS::RootedObject jsAnnotation(cx, JS_NewPlainObject(cx));
+    if (!jsAnnotation) {
+      continue;
+    }
+    // Build a key to index the current annotations in our hash set.
+    nsAutoString annotationsKey;
+    nsresult rv = ComputeAnnotationsKey(curAnnotations, annotationsKey);
+    if (NS_FAILED(rv)) {
+      continue;
+    }
+    // Check if the annotations are in the set. If that's the case, don't double report.
+    if (reportedAnnotations.GetEntry(annotationsKey)) {
+      continue;
+    }
+    // If not, report them.
+    reportedAnnotations.PutEntry(annotationsKey);
+    UniquePtr<HangAnnotations::Enumerator> annotationsEnum =
+      curAnnotations->GetEnumerator();
+    if (!annotationsEnum) {
+      continue;
+    }
+    nsAutoString key;
+    nsAutoString value;
+    while (annotationsEnum->Next(key, value)) {
+      JS::RootedValue jsValue(cx);
+      jsValue.setString(JS_NewUCStringCopyN(cx, value.get(), value.Length()));
+      if (!JS_DefineUCProperty(cx, jsAnnotation, key.get(), key.Length(),
+                               jsValue, JSPROP_ENUMERATE)) {
+        returnedObject.set(nullptr);
+        return;
+      }
+    }
+    if (!JS_SetElement(cx, annotationsArray, annotationIndex, jsAnnotation)) {
+      continue;
+    }
+    ++annotationIndex;
+  }
+  // Return the array using a |MutableHandleObject| to avoid triggering a false
+  // positive rooting issue in the hazard analysis build.
+  returnedObject.set(annotationsArray);
+}
+
+static JSObject*
+CreateJSHangHistogram(JSContext* cx, const Telemetry::HangHistogram& hang)
+{
+  JS::RootedObject ret(cx, JS_NewPlainObject(cx));
+  if (!ret) {
+    return nullptr;
+  }
+
+  JS::RootedObject stack(cx, CreateJSHangStack(cx, hang.GetStack()));
+  JS::RootedObject time(cx, CreateJSTimeHistogram(cx, hang));
+  auto& hangAnnotations = hang.GetAnnotations();
+  JS::RootedObject annotations(cx);
+  CreateJSHangAnnotations(cx, hangAnnotations, &annotations);
+
+  if (!stack ||
+      !time ||
+      !annotations ||
+      !JS_DefineProperty(cx, ret, "stack", stack, JSPROP_ENUMERATE) ||
+      !JS_DefineProperty(cx, ret, "histogram", time, JSPROP_ENUMERATE) ||
+      (!hangAnnotations.empty() && // <-- Only define annotations when nonempty
+        !JS_DefineProperty(cx, ret, "annotations", annotations, JSPROP_ENUMERATE))) {
+    return nullptr;
+  }
+
+  return ret;
+}
+
+} // namespace
+
 namespace mozilla {
 namespace Telemetry {
 
+JSObject*
+CreateJSThreadHangStats(JSContext* cx, const Telemetry::ThreadHangStats& thread)
+{
+  JS::RootedObject ret(cx, JS_NewPlainObject(cx));
+  if (!ret) {
+    return nullptr;
+  }
+  JS::RootedString name(cx, JS_NewStringCopyZ(cx, thread.GetName()));
+  if (!name ||
+      !JS_DefineProperty(cx, ret, "name", name, JSPROP_ENUMERATE)) {
+    return nullptr;
+  }
+
+  JS::RootedObject activity(cx, CreateJSTimeHistogram(cx, thread.mActivity));
+  if (!activity ||
+      !JS_DefineProperty(cx, ret, "activity", activity, JSPROP_ENUMERATE)) {
+    return nullptr;
+  }
+
+  // Process the hangs into a hangs object.
+  JS::RootedObject hangs(cx, JS_NewArrayObject(cx, 0));
+  if (!hangs) {
+    return nullptr;
+  }
+  for (size_t i = 0; i < thread.mHangs.length(); i++) {
+    JS::RootedObject obj(cx, CreateJSHangHistogram(cx, thread.mHangs[i]));
+    if (!ret) {
+      return nullptr;
+    }
+
+    JS::RootedString runnableName(cx, JS_NewStringCopyZ(cx, thread.mHangs[i].GetRunnableName()));
+    if (!runnableName ||
+        !JS_DefineProperty(cx, ret, "runnableName", runnableName, JSPROP_ENUMERATE)) {
+      return nullptr;
+    }
+
+    // Check if we have a cached native stack index, and if we do record it.
+    uint32_t index = thread.mHangs[i].GetNativeStackIndex();
+    if (index != Telemetry::HangHistogram::NO_NATIVE_STACK_INDEX) {
+      if (!JS_DefineProperty(cx, obj, "nativeStack", index, JSPROP_ENUMERATE)) {
+        return nullptr;
+      }
+    }
+
+    if (!JS_DefineElement(cx, hangs, i, obj, JSPROP_ENUMERATE)) {
+      return nullptr;
+    }
+  }
+  if (!JS_DefineProperty(cx, ret, "hangs", hangs, JSPROP_ENUMERATE)) {
+    return nullptr;
+  }
+
+  // We should already have a CombinedStacks object on the ThreadHangStats, so
+  // add that one.
+  JS::RootedObject fullReportObj(cx, CreateJSStackObject(cx, thread.mCombinedStacks));
+  if (!fullReportObj) {
+    return nullptr;
+  }
+
+  if (!JS_DefineProperty(cx, ret, "nativeStacks", fullReportObj, JSPROP_ENUMERATE)) {
+    return nullptr;
+  }
+
+  return ret;
+}
+
+void
+TimeHistogram::Add(PRIntervalTime aTime)
+{
+  uint32_t timeMs = PR_IntervalToMilliseconds(aTime);
+  size_t index = mozilla::FloorLog2(timeMs);
+  operator[](index)++;
+}
+
 const char*
 HangStack::InfallibleAppendViaBuffer(const char* aText, size_t aLength)
 {
   MOZ_ASSERT(this->canAppendWithoutRealloc(1));
   // Include null-terminator in length count.
   MOZ_ASSERT(mBuffer.canAppendWithoutRealloc(aLength + 1));
 
   const char* const entry = mBuffer.end();
@@ -50,10 +283,40 @@ HangStack::AppendViaBuffer(const char* a
         entry += mBuffer.begin() - prevStart;
       }
     }
   }
 
   return InfallibleAppendViaBuffer(aText, aLength);
 }
 
+uint32_t
+HangHistogram::GetHash(const HangStack& aStack)
+{
+  uint32_t hash = 0;
+  for (const char* const* label = aStack.begin();
+       label != aStack.end(); label++) {
+    /* If the string is within our buffer, we need to hash its content.
+       Otherwise, the string is statically allocated, and we only need
+       to hash the pointer instead of the content. */
+    if (aStack.IsInBuffer(*label)) {
+      hash = AddToHash(hash, HashString(*label));
+    } else {
+      hash = AddToHash(hash, *label);
+    }
+  }
+  return hash;
+}
+
+bool
+HangHistogram::operator==(const HangHistogram& aOther) const
+{
+  if (mHash != aOther.mHash) {
+    return false;
+  }
+  if (mStack.length() != aOther.mStack.length()) {
+    return false;
+  }
+  return mStack == aOther.mStack;
+}
+
 } // namespace Telemetry
 } // namespace mozilla
--- a/toolkit/components/telemetry/ThreadHangStats.h
+++ b/toolkit/components/telemetry/ThreadHangStats.h
@@ -23,16 +23,41 @@ namespace mozilla {
 namespace Telemetry {
 
 // This variable controls the maximum number of native hang stacks which may be
 // attached to a ping. This is due to how large native stacks can be. We want to
 // reduce the chance of a ping being discarded due to it exceeding the maximum
 // ping size.
 static const uint32_t kMaximumNativeHangStacks = 300;
 
+static const size_t kTimeHistogramBuckets = 8 * sizeof(PRIntervalTime);
+
+/* TimeHistogram is an efficient histogram that puts time durations into
+   exponential (base 2) buckets; times are accepted in PRIntervalTime and
+   stored in milliseconds. */
+class TimeHistogram : public mozilla::Array<uint32_t, kTimeHistogramBuckets>
+{
+public:
+  TimeHistogram()
+  {
+    mozilla::PodArrayZero(*this);
+  }
+  // Get minimum (inclusive) range of bucket in milliseconds
+  uint32_t GetBucketMin(size_t aBucket) const {
+    MOZ_ASSERT(aBucket < ArrayLength(*this));
+    return (1u << aBucket) & ~1u; // Bucket 0 starts at 0, not 1
+  }
+  // Get maximum (inclusive) range of bucket in milliseconds
+  uint32_t GetBucketMax(size_t aBucket) const {
+    MOZ_ASSERT(aBucket < ArrayLength(*this));
+    return (1u << (aBucket + 1u)) - 1u;
+  }
+  void Add(PRIntervalTime aTime);
+};
+
 /* A native stack is a simple list of pointers, so rather than building a
    wrapper type, we typdef the type here. */
 typedef std::vector<uintptr_t> NativeHangStack;
 
 /* HangStack stores an array of const char pointers,
    with optional internal storage for strings. */
 class HangStack
 {
@@ -130,12 +155,135 @@ public:
     return mBuffer.reserve(aCapacity) &&
            mBuffer.reserve(mBuffer.capacity());
   }
 
   const char* InfallibleAppendViaBuffer(const char* aText, size_t aLength);
   const char* AppendViaBuffer(const char* aText, size_t aLength);
 };
 
+/* A hang histogram consists of a stack associated with the
+   hang, along with a time histogram of the hang times. */
+class HangHistogram : public TimeHistogram
+{
+public:
+  // Value used for mNativeStackIndex to represent the absence of a cached
+  // native stack.
+  static const uint32_t NO_NATIVE_STACK_INDEX = UINT32_MAX;
+
+private:
+  static uint32_t GetHash(const HangStack& aStack);
+
+  HangStack mStack;
+  // Cached index of the native stack in the mCombinedStacks list in the owning
+  // ThreadHangStats object. A default value of NO_NATIVE_STACK_INDEX means that
+  // the ThreadHangStats object which owns this HangHistogram doesn't have a
+  // cached CombinedStacks with this HangHistogram in it.
+  uint32_t mNativeStackIndex;
+  // Use a hash to speed comparisons
+  const uint32_t mHash;
+  // Annotations attributed to this stack
+  HangMonitor::HangAnnotationsVector mAnnotations;
+  // The name of the runnable on the current thread.
+  nsCString mRunnableName;
+
+public:
+  explicit HangHistogram(HangStack&& aStack, const nsACString& aRunnableName)
+    : mStack(mozilla::Move(aStack))
+    , mNativeStackIndex(NO_NATIVE_STACK_INDEX)
+    , mHash(GetHash(mStack))
+    , mRunnableName(aRunnableName)
+  {
+  }
+
+  HangHistogram(HangHistogram&& aOther)
+    : TimeHistogram(mozilla::Move(aOther))
+    , mStack(mozilla::Move(aOther.mStack))
+    , mNativeStackIndex(mozilla::Move(aOther.mNativeStackIndex))
+    , mHash(mozilla::Move(aOther.mHash))
+    , mAnnotations(mozilla::Move(aOther.mAnnotations))
+    , mRunnableName(aOther.mRunnableName)
+  {
+  }
+  bool operator==(const HangHistogram& aOther) const;
+  bool operator!=(const HangHistogram& aOther) const
+  {
+    return !operator==(aOther);
+  }
+  const HangStack& GetStack() const {
+    return mStack;
+  }
+  uint32_t GetNativeStackIndex() const {
+    return mNativeStackIndex;
+  }
+  void SetNativeStackIndex(uint32_t aIndex) {
+    MOZ_ASSERT(aIndex != NO_NATIVE_STACK_INDEX);
+    mNativeStackIndex = aIndex;
+  }
+  const char* GetRunnableName() const {
+    return mRunnableName.get();
+  }
+  const HangMonitor::HangAnnotationsVector& GetAnnotations() const {
+    return mAnnotations;
+  }
+  void Add(PRIntervalTime aTime, HangMonitor::HangAnnotationsPtr aAnnotations) {
+    TimeHistogram::Add(aTime);
+    if (aAnnotations) {
+      if (!mAnnotations.append(Move(aAnnotations))) {
+        MOZ_CRASH();
+      }
+    }
+  }
+};
+
+/* Thread hang stats consist of
+ - thread name
+ - time histogram of all task run times
+ - hang histograms of individual hangs
+ - annotations for each hang
+ - combined native stacks for all hangs
+*/
+class ThreadHangStats
+{
+private:
+  nsCString mName;
+
+public:
+  TimeHistogram mActivity;
+  mozilla::Vector<HangHistogram, 4> mHangs;
+  uint32_t mNativeStackCnt;
+  CombinedStacks mCombinedStacks;
+
+  explicit ThreadHangStats(const char* aName)
+    : mName(aName)
+    , mNativeStackCnt(0)
+    , mCombinedStacks(Telemetry::kMaximumNativeHangStacks)
+  {
+  }
+  ThreadHangStats(ThreadHangStats&& aOther)
+    : mName(mozilla::Move(aOther.mName))
+    , mActivity(mozilla::Move(aOther.mActivity))
+    , mHangs(mozilla::Move(aOther.mHangs))
+    , mNativeStackCnt(aOther.mNativeStackCnt)
+    , mCombinedStacks(mozilla::Move(aOther.mCombinedStacks))
+  {
+    aOther.mNativeStackCnt = 0;
+  }
+  const char* GetName() const {
+    return mName.get();
+  }
+};
+
+/**
+ * Reflects thread hang stats object as a JS object.
+ *
+ * @param JSContext* cx javascript context.
+ * @param JSContext* cx thread hang statistics.
+ *
+ * @return JSObject* Javascript reflection of the statistics.
+ */
+JSObject*
+CreateJSThreadHangStats(JSContext* cx, const Telemetry::ThreadHangStats& thread);
+
 } // namespace Telemetry
 } // namespace mozilla
 
 #endif // mozilla_BackgroundHangTelemetry_h
--- a/toolkit/components/telemetry/docs/data/main-ping.rst
+++ b/toolkit/components/telemetry/docs/data/main-ping.rst
@@ -58,16 +58,17 @@ Structure:
       processes: {...},
       childPayloads: [...], // only present with e10s; reduced payloads from content processes, null on failure
       simpleMeasurements: {...},
 
       // The following properties may all be null if we fail to collect them.
       histograms: {...},
       keyedHistograms: {...},
       chromeHangs: {...},
+      threadHangStats: [...],
       capturedStacks: {...},
       log: [...],
       webrtc: {...},
       gc: {...},
       fileIOReports: {...},
       lateWrites: {...},
       addonDetails: {...},
       UIMeasurements: [...],  // Android only
@@ -242,16 +243,76 @@ histograms
 This section contains the histograms that are valid for the current platform. ``Flag`` and ``count`` histograms are always created and submitted, with their default value being respectively ``false`` and ``0``. Other histogram types (see :ref:`choosing-histogram-type`) are not created nor submitted if no data was added to them. The type and format of the reported histograms is described by the ``Histograms.json`` file. Its most recent version is available `here <https://dxr.mozilla.org/mozilla-central/source/toolkit/components/telemetry/Histograms.json>`_. The ``info.revision`` field indicates the revision of the file that describes the reported histograms.
 
 keyedHistograms
 ---------------
 This section contains the keyed histograms available for the current platform.
 
 As of Firefox 48, this section does not contain empty keyed histograms anymore.
 
+threadHangStats
+---------------
+Contains the statistics about the hangs in main and background threads. Note that hangs in this section capture the `C++ pseudostack <https://developer.mozilla.org/en-US/docs/Mozilla/Performance/Profiling_with_the_Built-in_Profiler#Native_stack_vs._Pseudo_stack>`_ and an incomplete JS stack, which is not 100% precise. For particularly egregious hangs, and on nightly, an unsymbolicated native stack is also captured. The amount of time that is considered "egregious" is different from thread to thread, and is set when the BackgroundHangMonitor is constructed for that thread. In general though, hangs from 5 - 10 seconds are generally considered egregious. Shorter hangs (1 - 2s) are considered egregious for other threads (the compositor thread, and the hang monitor that is only enabled during tab switch).
+
+To avoid submitting overly large payloads, some limits are applied:
+
+* Identical, adjacent "(chrome script)" or "(content script)" stack entries are collapsed together. If a stack is reduced, the "(reduced stack)" frame marker is added as the oldest frame.
+* The depth of the reported pseudostacks is limited to 11 entries. This value represents the 99.9th percentile of the thread hangs stack depths reported by Telemetry.
+* The native stacks are limited to a depth of 25 stack frames.
+
+Structure:
+
+.. code-block:: js
+
+    "threadHangStats" : [
+      {
+        "name" : "Gecko",
+        "activity" : {...}, // a time histogram of all task run times
+        "nativeStacks": { // captured for all hangs on nightly, or egregious hangs on beta
+          "memoryMap": [
+            ["wgdi32.pdb", "08A541B5942242BDB4AEABD8C87E4CFF2"],
+            ["igd10iumd32.pdb", "D36DEBF2E78149B5BE1856B772F1C3991"],
+            // ... other entries in the format ["module name", "breakpad identifier"] ...
+          ],
+          "stacks": [
+            [
+              [
+                0, // the module index or -1 for invalid module indices
+                190649 // the offset of this program counter in its module or an absolute pc
+              ],
+              [1, 2540075],
+              // ... other frames ...
+            ],
+            // ... other stacks ...
+          ]
+        },
+        "hangs" : [
+          {
+            "stack" : [
+              "Startup::XRE_Main",
+              "Timer::Fire",
+              "(content script)",
+              "IPDL::PPluginScriptableObject::SendGetChildProperty",
+              ... up to 11 frames ...
+            ],
+            "nativeStack": 0, // index into nativeStacks.stacks array
+            "histogram" : {...}, // the time histogram of the hang times
+            "annotations" : [
+              {
+                "pluginName" : "Shockwave Flash",
+                "pluginVersion" : "18.0.0.209"
+              },
+              ... other annotations ...
+            ]
+          },
+        ],
+      },
+      ... other threads ...
+    ]
+
 capturedStacks
 --------------
 Contains information about stacks captured on demand via Telemetry API. For more
 information see :doc:`stack capture <../collection/stack-capture>`.
 
 This is similar to :ref:`chromeHangs`, but only Precise C++ stacks on the main thread of
 the parent process are reported. This data is only available on Windows, either
 in Firefox Nightly or in builds using ``--enable-profiling`` switch.
--- a/toolkit/components/telemetry/nsITelemetry.idl
+++ b/toolkit/components/telemetry/nsITelemetry.idl
@@ -198,16 +198,33 @@ interface nsITelemetry : nsISupports
    * @return A promise that resolves to an array of modules or rejects with
              NS_ERROR_FAILURE on failure.
    * @throws NS_ERROR_NOT_IMPLEMENTED if the Gecko profiler is not enabled.
    */
   [implicit_jscontext]
   nsISupports getLoadedModules();
 
   /*
+   * An array of thread hang stats,
+   *   [<thread>, <thread>, ...]
+   * <thread> represents a single thread,
+   *   {"name": "<name>",
+   *    "activity": <time>,
+   *    "hangs": [<hang>, <hang>, ...]}
+   * <time> represents a histogram of time intervals in milliseconds,
+   *   with the same format as histogramSnapshots
+   * <hang> represents a particular hang,
+   *   {"stack": <stack>, "nativeStack": <stack>, "histogram": <time>}
+   * <stack> represents the hang's stack,
+   *   ["<frame_0>", "<frame_1>", ...]
+   */
+  [implicit_jscontext]
+  readonly attribute jsval threadHangStats;
+
+  /*
    * An object with two fields: memoryMap and stacks.
    * * memoryMap is a list of loaded libraries.
    * * stacks is a list of stacks. Each stack is a list of pairs of the form
    *   [moduleIndex, offset]. The moduleIndex is an index into the memoryMap and
    *   offset is an offset in the library at memoryMap[moduleIndex].
    * This format is used to make it easier to send the stacks to the
    * symbolication server.
    */
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -1975,17 +1975,17 @@ add_task(async function test_schedulerNo
   Assert.ok(!(await OS.File.exists(ABORTED_FILE)));
 
   await TelemetryController.testShutdown();
   PingServer.resetPingHandler();
 });
 
 add_task(async function test_pingExtendedStats() {
   const EXTENDED_PAYLOAD_FIELDS = [
-    "chromeHangs", "log", "slowSQL", "fileIOReports", "lateWrites",
+    "chromeHangs", "threadHangStats", "log", "slowSQL", "fileIOReports", "lateWrites",
     "addonDetails", "webrtc"
   ];
 
   if (AppConstants.platform == "android") {
     EXTENDED_PAYLOAD_FIELDS.push("UIMeasurements");
   }
 
   // Reset telemetry and disable sending extended statistics.
--- a/xpcom/threads/BackgroundHangMonitor.cpp
+++ b/xpcom/threads/BackgroundHangMonitor.cpp
@@ -44,16 +44,18 @@ static const size_t kMaxThreadHangStackD
 bool StackScriptEntriesCollapser(const char* aStackEntry, const char *aAnotherStackEntry)
 {
   return !strcmp(aStackEntry, aAnotherStackEntry) &&
          (!strcmp(aStackEntry, "(chrome script)") || !strcmp(aStackEntry, "(content script)"));
 }
 
 namespace mozilla {
 
+class ProcessHangRunnable;
+
 /**
  * BackgroundHangManager is the global object that
  * manages all instances of BackgroundHangThread.
  */
 class BackgroundHangManager : public nsIObserver
 {
 private:
   // Background hang monitor thread function
@@ -184,26 +186,27 @@ public:
   // Is the thread dedicated to a single BackgroundHangMonitor
   BackgroundHangMonitor::ThreadType mThreadType;
   // Platform-specific helper to get hang stacks
   ThreadStackHelper mStackHelper;
   // Stack of current hang
   Telemetry::HangStack mHangStack;
   // Native stack of current hang
   Telemetry::NativeHangStack mNativeHangStack;
+  // Statistics for telemetry
+  Telemetry::ThreadHangStats mStats;
   // Annotations for the current hang
   UniquePtr<HangMonitor::HangAnnotations> mAnnotations;
   // Annotators registered for this thread
   HangMonitor::Observer::Annotators mAnnotators;
+  // List of runnables which can hold a reference to us which need to be
+  // canceled before we can go away.
+  LinkedList<RefPtr<ProcessHangRunnable>> mProcessHangRunnables;
   // The name of the runnable which is hanging the current process
   nsCString mRunnableName;
-  // The name of the thread which is being monitored
-  nsCString mThreadName;
-  // The number of native stacks which have been collected so far.
-  uint32_t mNativeStackCnt;
 
   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.
@@ -365,20 +368,20 @@ BackgroundHangManager::RunMonitorThread(
         currentThread->ReportPermaHang();
         continue;
       }
 
       if (MOZ_LIKELY(!currentThread->mHanging)) {
         if (MOZ_UNLIKELY(hangTime >= currentThread->mTimeout)) {
           // A hang started
 #ifdef NIGHTLY_BUILD
-          if (currentThread->mNativeStackCnt < Telemetry::kMaximumNativeHangStacks) {
+          if (currentThread->mStats.mNativeStackCnt < Telemetry::kMaximumNativeHangStacks) {
             // NOTE: In nightly builds of firefox we want to collect native stacks
             // for all hangs, not just permahangs.
-            currentThread->mNativeStackCnt += 1;
+            currentThread->mStats.mNativeStackCnt += 1;
             currentThread->mStackHelper.GetPseudoAndNativeStack(
               currentThread->mHangStack,
               currentThread->mNativeHangStack,
               currentThread->mRunnableName);
           } else {
             currentThread->mStackHelper.GetPseudoStack(currentThread->mHangStack,
                                                        currentThread->mRunnableName);
           }
@@ -438,43 +441,140 @@ BackgroundHangThread::BackgroundHangThre
   , mMaxTimeout(aMaxTimeoutMs == BackgroundHangMonitor::kNoTimeout
                 ? PR_INTERVAL_NO_TIMEOUT
                 : PR_MillisecondsToInterval(aMaxTimeoutMs))
   , mInterval(mManager->mIntervalNow)
   , mHangStart(mInterval)
   , mHanging(false)
   , mWaiting(true)
   , mThreadType(aThreadType)
-  , mThreadName(aName)
-  , mNativeStackCnt(0)
+  , mStats(aName)
 {
   if (sTlsKeyInitialized && IsShared()) {
     sTlsKey.set(this);
   }
   // Lock here because LinkedList is not thread-safe
   MonitorAutoLock autoLock(mManager->mLock);
   // Add to thread list
   mManager->mHangThreads.insertBack(this);
   // Wake up monitor thread to process new thread
   autoLock.Notify();
 }
 
+// This runnable is used to pre-process a hang, performing any expensive
+// operations on it, before submitting it into the BackgroundHangThread object
+// for Telemetry.
+//
+// If this object is canceled, it will submit its payload to the
+// BackgroundHangThread without performing the processing.
+class ProcessHangRunnable final
+  : public CancelableRunnable
+  , public LinkedListElement<RefPtr<ProcessHangRunnable>>
+{
+public:
+  ProcessHangRunnable(BackgroundHangManager* aManager,
+                      BackgroundHangThread* aThread,
+                      Telemetry::HangHistogram&& aHistogram,
+                      Telemetry::NativeHangStack&& aNativeStack)
+    : CancelableRunnable("ProcessHangRunnable")
+    , mManager(aManager)
+    , mNativeStack(mozilla::Move(aNativeStack))
+    , mThread(aThread)
+    , mHistogram(mozilla::Move(aHistogram))
+  {
+    MOZ_ASSERT(mThread);
+  }
+
+  NS_IMETHOD
+  Run() override
+  {
+    // Start processing this histogram's native hang stack before we try to lock
+    // anything, as we can do this without any locks held. This is the expensive
+    // part of the operation.
+    Telemetry::ProcessedStack processed;
+    if (!mNativeStack.empty()) {
+       processed = Telemetry::GetStackAndModules(mNativeStack);
+    }
+
+    // Lock the manager's lock, so that we can take a look at our mThread
+    {
+      MonitorAutoLock autoLock(mManager->mLock);
+      if (NS_WARN_IF(!mThread)) {
+        return NS_OK;
+      }
+
+      // If we have a stack, check if we can add it to combined stacks. This is
+      // a relatively cheap operation, and must occur with the lock held.
+      if (!mNativeStack.empty() &&
+          mThread->mStats.mCombinedStacks.GetStackCount() < Telemetry::kMaximumNativeHangStacks) {
+        mHistogram.SetNativeStackIndex(mThread->mStats.mCombinedStacks.AddStack(processed));
+      }
+
+      // Submit, remove ourselves from the list, and clear out mThread so we
+      // don't run again.
+      MOZ_ALWAYS_TRUE(mThread->mStats.mHangs.append(Move(mHistogram)));
+      remove();
+      mThread = nullptr;
+    }
+
+    return NS_OK;
+  }
+
+  // Submits hang, and removes from list.
+  nsresult
+  Cancel() override
+  {
+    mManager->mLock.AssertCurrentThreadOwns();
+    if (NS_WARN_IF(!mThread)) {
+      return NS_OK;
+    }
+
+    // Submit, remove ourselves from the list, and clear out mThread so we
+    // don't run again.
+    MOZ_ALWAYS_TRUE(mThread->mStats.mHangs.append(Move(mHistogram)));
+    if (isInList()) {
+      remove();
+    }
+    mThread = nullptr;
+    return NS_OK;
+  }
+
+private:
+  // These variables are constant after initialization, and do not need
+  // synchronization.
+  RefPtr<BackgroundHangManager> mManager;
+  const Telemetry::NativeHangStack mNativeStack;
+  // These variables are guarded by mManager->mLock.
+  BackgroundHangThread* MOZ_NON_OWNING_REF mThread; // Will Cancel us before it dies
+  Telemetry::HangHistogram mHistogram;
+};
+
 BackgroundHangThread::~BackgroundHangThread()
 {
   // Lock here because LinkedList is not thread-safe
   MonitorAutoLock autoLock(mManager->mLock);
   // Remove from thread list
   remove();
   // Wake up monitor thread to process removed thread
   autoLock.Notify();
 
   // We no longer have a thread
   if (sTlsKeyInitialized && IsShared()) {
     sTlsKey.set(nullptr);
   }
+
+  // Cancel any remaining process hang runnables, as they hold a weak reference
+  // into our mStats variable, which we're about to move.
+  while (RefPtr<ProcessHangRunnable> runnable = mProcessHangRunnables.popFirst()) {
+    runnable->Cancel();
+  }
+
+  // Record the ThreadHangStats for this thread before we go away. All stats
+  // should be in this method now, as we canceled any pending runnables.
+  Telemetry::RecordThreadHangStats(Move(mStats));
 }
 
 void
 BackgroundHangThread::ReportHang(PRIntervalTime aHangTime)
 {
   // Recovered from a hang; called on the monitor thread
   // mManager->mLock IS locked
 
@@ -497,30 +597,71 @@ BackgroundHangThread::ReportHang(PRInter
   if (mHangStack.length() > kMaxThreadHangStackDepth) {
     const int elementsToRemove = mHangStack.length() - kMaxThreadHangStackDepth;
     // Replace the oldest frame with a known label so that we can tell this stack
     // was limited.
     mHangStack[0] = "(reduced stack)";
     mHangStack.erase(mHangStack.begin() + 1, mHangStack.begin() + elementsToRemove);
   }
 
-  // XXX: HangDetails will be expanded to contain all of the relevant
-  // information and handle reporting a custom ping to telemetry.
+  Telemetry::HangHistogram newHistogram(Move(mHangStack), mRunnableName);
+  for (Telemetry::HangHistogram* oldHistogram = mStats.mHangs.begin();
+       oldHistogram != mStats.mHangs.end(); oldHistogram++) {
+    if (newHistogram == *oldHistogram) {
+      // New histogram matches old one
+      oldHistogram->Add(aHangTime, Move(mAnnotations));
+      return;
+    }
+  }
+  newHistogram.Add(aHangTime, Move(mAnnotations));
 
   // Notify any observers of the "bhr-thread-hang" topic that a thread has hung.
   nsCString name;
-  name.Assign(mThreadName);
+  name.AssignASCII(mStats.GetName());
   nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction("NotifyBHRHangObservers", [=] {
     nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
     if (os) {
       // NOTE: Make sure to construct this on the main thread.
       nsCOMPtr<nsIHangDetails> hangDetails = new HangDetails(aHangTime, name);
       os->NotifyObservers(hangDetails, "bhr-thread-hang", nullptr);
     }
   });
+  if (SystemGroup::Initialized()) {
+    // XXX(HACK): This is really sketchy. We need to keep a reference to the
+    // runnable in case the dispatch fails. If it fails, the already_AddRefed
+    // runnable which we passed in has been leaked, and we need to free it
+    // ourselves. The only time when this should fail is if we're shutting down.
+    //
+    // Most components just avoid dispatching runnables during shutdown, but BHR
+    // is not shut down until way too late, so we cannot do that. Instead, we
+    // just detect that the dispatch failed and manually unleak the leaked
+    // nsIRunnable in that situation.
+    nsresult rv = SystemGroup::Dispatch(TaskCategory::Other,
+                                        do_AddRef(runnable.get()));
+    if (NS_FAILED(rv)) {
+      // NOTE: We go through `get()` here in order to avoid the
+      // MOZ_NO_ADDREF_RELEASE_ON_RETURN static analysis.
+      nsrefcnt refcnt = runnable.get()->Release();
+      MOZ_RELEASE_ASSERT(refcnt == 1, "runnable should have had 1 reference leaked");
+    }
+  }
+
+  // Process the hang off-main thread. We record a reference to the runnable in
+  // mProcessHangRunnables so we can abort this preprocessing and just submit
+  // the message if the processing takes too long and our thread is going away.
+  RefPtr<ProcessHangRunnable> processHang =
+    new ProcessHangRunnable(mManager, this, Move(newHistogram), Move(mNativeHangStack));
+  mProcessHangRunnables.insertFront(processHang);
+
+  // Try to dispatch the runnable to the StreamTransportService threadpool. If
+  // we fail, cancel our runnable.
+  if (!mManager->mSTS || NS_FAILED(mManager->mSTS->Dispatch(processHang.forget()))) {
+    RefPtr<ProcessHangRunnable> runnable = mProcessHangRunnables.popFirst();
+    runnable->Cancel();
+  }
 }
 
 void
 BackgroundHangThread::ReportPermaHang()
 {
   // Permanently hanged; called on the monitor thread
   // mManager->mLock IS locked
 
@@ -540,16 +681,17 @@ BackgroundHangThread::Update()
   if (mWaiting) {
     mInterval = intervalNow;
     mWaiting = false;
     /* We have to wake up the manager thread because when all threads
        are waiting, the manager thread waits indefinitely as well. */
     mManager->Wakeup();
   } else {
     PRIntervalTime duration = intervalNow - mInterval;
+    mStats.mActivity.Add(duration);
     if (MOZ_UNLIKELY(duration >= mTimeout)) {
       /* Wake up the manager thread to tell it that a hang ended */
       mManager->Wakeup();
     }
     mInterval = intervalNow;
   }
 }
 
@@ -750,16 +892,43 @@ BackgroundHangMonitor::UnregisterAnnotat
     return false;
   }
   return thisThread->mAnnotators.Unregister(aAnnotator);
 #else
   return false;
 #endif
 }
 
+/* Because we are iterating through the BackgroundHangThread linked list,
+   we need to take a lock. Using MonitorAutoLock as a base class makes
+   sure all of that is taken care of for us. */
+BackgroundHangMonitor::ThreadHangStatsIterator::ThreadHangStatsIterator()
+  : MonitorAutoLock(BackgroundHangManager::sInstance->mLock)
+  , mThread(BackgroundHangManager::sInstance ?
+            BackgroundHangManager::sInstance->mHangThreads.getFirst() :
+            nullptr)
+{
+#ifdef MOZ_ENABLE_BACKGROUND_HANG_MONITOR
+  MOZ_ASSERT(BackgroundHangManager::sInstance ||
+             BackgroundHangManager::sDisabled,
+             "Inconsistent state");
+#endif
+}
+
+Telemetry::ThreadHangStats*
+BackgroundHangMonitor::ThreadHangStatsIterator::GetNext()
+{
+  if (!mThread) {
+    return nullptr;
+  }
+  Telemetry::ThreadHangStats* stats = &mThread->mStats;
+  mThread = mThread->getNext();
+  return stats;
+}
+
 NS_IMETHODIMP
 HangDetails::GetDuration(uint32_t* aDuration)
 {
   *aDuration = mDuration;
   return NS_OK;
 }
 
 NS_IMETHODIMP
--- a/xpcom/threads/BackgroundHangMonitor.h
+++ b/xpcom/threads/BackgroundHangMonitor.h
@@ -12,16 +12,20 @@
 #include "mozilla/RefPtr.h"
 
 #include "nsString.h"
 
 #include <stdint.h>
 
 namespace mozilla {
 
+namespace Telemetry {
+class ThreadHangStats;
+} // namespace Telemetry
+
 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
@@ -119,16 +123,54 @@ public:
     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
   };
 
   /**
+   * ThreadHangStatsIterator is used to iterate through the ThreadHangStats
+   * associated with each active monitored thread. Because of an internal
+   * lock while this object is alive, a thread must use only one instance
+   * of this class at a time and must iterate through the list as fast as
+   * possible. The following example shows using the iterator:
+   *
+   * {
+   *   // Scope the iter variable so it's destroyed as soon as we're done
+   *   BackgroundHangMonitor::ThreadHangStatsIterator iter;
+   *   for (ThreadHangStats* histogram = iter.GetNext();
+   *        histogram; histogram = iter.GetNext()) {
+   *     // Process histogram
+   *   }
+   * }
+   */
+  class ThreadHangStatsIterator : public MonitorAutoLock
+  {
+  private:
+    BackgroundHangThread* mThread;
+
+    ThreadHangStatsIterator(const ThreadHangStatsIterator&);
+    ThreadHangStatsIterator& operator=(const ThreadHangStatsIterator&);
+
+  public:
+    /**
+     * Create an ThreadHangStatsIterator instance and take the internal lock.
+     * Internal lock is released on destruction.
+     */
+    ThreadHangStatsIterator();
+
+    /**
+     * Get the next item in the list; the first call returns the first item.
+     * Returns nullptr at the end of the list.
+     */
+    Telemetry::ThreadHangStats* GetNext();
+  };
+
+  /**
    * Enable hang monitoring.
    * Must return before using BackgroundHangMonitor.
    */
   static void Startup();
 
   /**
    * Disable hang monitoring.
    * Can be called without destroying all BackgroundHangMonitors first.