Bug 1335461: Pulling out stack capturing into standalone files. r?gfritzsche draft
authorIaroslav (yarik) Sheptykin <yarik.sheptykin@gmail.com>
Mon, 15 May 2017 10:45:02 +0200
changeset 579262 7a48c83ffa012a42c94f1a0beeb8948b6723afcb
parent 577554 e66dedabe582ba7b394aee4f89ed70fe389b3c46
child 628940 5df14de6e51587dd4cb57e5f16f7dda373b52de8
push id59179
push userbmo:yarik.sheptykin@googlemail.com
push dateWed, 17 May 2017 05:58:08 +0000
reviewersgfritzsche
bugs1335461
milestone55.0a1
Bug 1335461: Pulling out stack capturing into standalone files. r?gfritzsche MozReview-Commit-ID: 7HY0cCmfOYi
toolkit/components/telemetry/CombinedStacks.cpp
toolkit/components/telemetry/CombinedStacks.h
toolkit/components/telemetry/HangReports.cpp
toolkit/components/telemetry/HangReports.h
toolkit/components/telemetry/KeyedStackCapturer.cpp
toolkit/components/telemetry/KeyedStackCapturer.h
toolkit/components/telemetry/Telemetry.cpp
toolkit/components/telemetry/moz.build
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/CombinedStacks.cpp
@@ -0,0 +1,196 @@
+/* -*- 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 "CombinedStacks.h"
+#include "HangAnnotations.h"
+#include "mozilla/HangAnnotations.h"
+#include "jsapi.h"
+
+namespace mozilla {
+namespace Telemetry {
+
+// The maximum number of chrome hangs stacks that we're keeping.
+const size_t kMaxChromeStacksKept = 50;
+
+size_t
+CombinedStacks::GetModuleCount() const {
+  return mModules.size();
+}
+
+const Telemetry::ProcessedStack::Module&
+CombinedStacks::GetModule(unsigned aIndex) const {
+  return mModules[aIndex];
+}
+
+size_t
+CombinedStacks::AddStack(const Telemetry::ProcessedStack& aStack) {
+  // Advance the indices of the circular queue holding the stacks.
+  size_t index = mNextIndex++ % kMaxChromeStacksKept;
+  // Grow the vector up to the maximum size, if needed.
+  if (mStacks.size() < kMaxChromeStacksKept) {
+    mStacks.resize(mStacks.size() + 1);
+  }
+  // Get a reference to the location holding the new stack.
+  CombinedStacks::Stack& adjustedStack = mStacks[index];
+  // If we're using an old stack to hold aStack, clear it.
+  adjustedStack.clear();
+
+  size_t stackSize = aStack.GetStackSize();
+  for (size_t i = 0; i < stackSize; ++i) {
+    const Telemetry::ProcessedStack::Frame& frame = aStack.GetFrame(i);
+    uint16_t modIndex;
+    if (frame.mModIndex == std::numeric_limits<uint16_t>::max()) {
+      modIndex = frame.mModIndex;
+    } else {
+      const Telemetry::ProcessedStack::Module& module =
+        aStack.GetModule(frame.mModIndex);
+      std::vector<Telemetry::ProcessedStack::Module>::iterator modIterator =
+        std::find(mModules.begin(), mModules.end(), module);
+      if (modIterator == mModules.end()) {
+        mModules.push_back(module);
+        modIndex = mModules.size() - 1;
+      } else {
+        modIndex = modIterator - mModules.begin();
+      }
+    }
+    Telemetry::ProcessedStack::Frame adjustedFrame = { frame.mOffset, modIndex };
+    adjustedStack.push_back(adjustedFrame);
+  }
+  return index;
+}
+
+const CombinedStacks::Stack&
+CombinedStacks::GetStack(unsigned aIndex) const {
+  return mStacks[aIndex];
+}
+
+size_t
+CombinedStacks::GetStackCount() const {
+  return mStacks.size();
+}
+
+size_t
+CombinedStacks::SizeOfExcludingThis() const {
+  // This is a crude approximation. We would like to do something like
+  // aMallocSizeOf(&mModules[0]), but on linux aMallocSizeOf will call
+  // malloc_usable_size which is only safe on the pointers returned by malloc.
+  // While it works on current libstdc++, it is better to be safe and not assume
+  // that &vec[0] points to one. We could use a custom allocator, but
+  // it doesn't seem worth it.
+  size_t n = 0;
+  n += mModules.capacity() * sizeof(Telemetry::ProcessedStack::Module);
+  n += mStacks.capacity() * sizeof(Stack);
+  for (const auto & s : mStacks) {
+    n += s.capacity() * sizeof(Telemetry::ProcessedStack::Frame);
+  }
+  return n;
+}
+
+#if defined(ENABLE_STACK_CAPTURE)
+void
+CombinedStacks::Clear() {
+  mNextIndex = 0;
+  mStacks.clear();
+  mModules.clear();
+}
+#endif
+
+JSObject *
+CreateJSStackObject(JSContext *cx, const CombinedStacks &stacks) {
+  JS::Rooted<JSObject*> ret(cx, JS_NewPlainObject(cx));
+  if (!ret) {
+    return nullptr;
+  }
+
+  JS::Rooted<JSObject*> moduleArray(cx, JS_NewArrayObject(cx, 0));
+  if (!moduleArray) {
+    return nullptr;
+  }
+  bool ok = JS_DefineProperty(cx, ret, "memoryMap", moduleArray,
+                              JSPROP_ENUMERATE);
+  if (!ok) {
+    return nullptr;
+  }
+
+  const size_t moduleCount = stacks.GetModuleCount();
+  for (size_t moduleIndex = 0; moduleIndex < moduleCount; ++moduleIndex) {
+    // Current module
+    const Telemetry::ProcessedStack::Module& module =
+      stacks.GetModule(moduleIndex);
+
+    JS::Rooted<JSObject*> moduleInfoArray(cx, JS_NewArrayObject(cx, 0));
+    if (!moduleInfoArray) {
+      return nullptr;
+    }
+    if (!JS_DefineElement(cx, moduleArray, moduleIndex, moduleInfoArray,
+                          JSPROP_ENUMERATE)) {
+      return nullptr;
+    }
+
+    unsigned index = 0;
+
+    // Module name
+    JS::Rooted<JSString*> str(cx, JS_NewUCStringCopyZ(cx, module.mName.get()));
+    if (!str || !JS_DefineElement(cx, moduleInfoArray, index++, str, JSPROP_ENUMERATE)) {
+      return nullptr;
+    }
+
+    // Module breakpad identifier
+    JS::Rooted<JSString*> id(cx, JS_NewStringCopyZ(cx, module.mBreakpadId.c_str()));
+    if (!id || !JS_DefineElement(cx, moduleInfoArray, index++, id, JSPROP_ENUMERATE)) {
+      return nullptr;
+    }
+  }
+
+  JS::Rooted<JSObject*> reportArray(cx, JS_NewArrayObject(cx, 0));
+  if (!reportArray) {
+    return nullptr;
+  }
+  ok = JS_DefineProperty(cx, ret, "stacks", reportArray, JSPROP_ENUMERATE);
+  if (!ok) {
+    return nullptr;
+  }
+
+  const size_t length = stacks.GetStackCount();
+  for (size_t i = 0; i < length; ++i) {
+    // Represent call stack PCs as (module index, offset) pairs.
+    JS::Rooted<JSObject*> pcArray(cx, JS_NewArrayObject(cx, 0));
+    if (!pcArray) {
+      return nullptr;
+    }
+
+    if (!JS_DefineElement(cx, reportArray, i, pcArray, JSPROP_ENUMERATE)) {
+      return nullptr;
+    }
+
+    const CombinedStacks::Stack& stack = stacks.GetStack(i);
+    const uint32_t pcCount = stack.size();
+    for (size_t pcIndex = 0; pcIndex < pcCount; ++pcIndex) {
+      const Telemetry::ProcessedStack::Frame& frame = stack[pcIndex];
+      JS::Rooted<JSObject*> framePair(cx, JS_NewArrayObject(cx, 0));
+      if (!framePair) {
+        return nullptr;
+      }
+      int modIndex = (std::numeric_limits<uint16_t>::max() == frame.mModIndex) ?
+        -1 : frame.mModIndex;
+      if (!JS_DefineElement(cx, framePair, 0, modIndex, JSPROP_ENUMERATE)) {
+        return nullptr;
+      }
+      if (!JS_DefineElement(cx, framePair, 1, static_cast<double>(frame.mOffset),
+                            JSPROP_ENUMERATE)) {
+        return nullptr;
+      }
+      if (!JS_DefineElement(cx, pcArray, pcIndex, framePair, JSPROP_ENUMERATE)) {
+        return nullptr;
+      }
+    }
+  }
+
+  return ret;
+}
+
+} // namespace Telemetry
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/CombinedStacks.h
@@ -0,0 +1,56 @@
+/* -*-  Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CombinedStacks_h__
+#define CombinedStacks_h__
+
+#include <vector>
+
+#include "ProcessedStack.h"
+
+class JSObject;
+class JSContext;
+
+namespace mozilla {
+namespace Telemetry {
+
+/**
+ * This class is conceptually a list of ProcessedStack objects, but it represents them
+ * more efficiently by keeping a single global list of modules.
+ */
+class CombinedStacks {
+public:
+  CombinedStacks() : mNextIndex(0) {}
+  typedef std::vector<Telemetry::ProcessedStack::Frame> Stack;
+  const Telemetry::ProcessedStack::Module& GetModule(unsigned aIndex) const;
+  size_t GetModuleCount() const;
+  const Stack& GetStack(unsigned aIndex) const;
+  size_t AddStack(const Telemetry::ProcessedStack& aStack);
+  size_t GetStackCount() const;
+  size_t SizeOfExcludingThis() const;
+
+  #if defined(ENABLE_STACK_CAPTURE)
+    /** Clears the contents of vectors and resets the index. */
+    void Clear();
+  #endif
+
+private:
+  std::vector<Telemetry::ProcessedStack::Module> mModules;
+  // A circular buffer to hold the stacks.
+  std::vector<Stack> mStacks;
+  // The index of the next buffer element to write to in mStacks.
+  size_t mNextIndex;
+};
+
+/**
+ * Creates a JSON representation of given combined stacks object.
+ */
+JSObject *
+CreateJSStackObject(JSContext *cx, const CombinedStacks &stacks);
+
+} // namespace Telemetry
+} // namespace mozilla
+
+#endif // CombinedStacks_h__
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/HangReports.cpp
@@ -0,0 +1,156 @@
+/* -*- 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 "HangReports.h"
+
+namespace mozilla {
+namespace Telemetry {
+
+using namespace HangMonitor;
+
+/** The maximum number of stacks that we're keeping for hang reports. */
+const size_t kMaxHangStacksKept = 50;
+
+// This utility function generates a string key that is used to index the annotations
+// in a hash map from |HangReports::AddHang|.
+nsresult
+ComputeAnnotationsKey(const HangAnnotationsPtr& aAnnotations, nsAString& aKeyOut)
+{
+  UniquePtr<HangAnnotations::Enumerator> annotationsEnum = aAnnotations->GetEnumerator();
+  if (!annotationsEnum) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // Append all the attributes to the key, to uniquely identify this annotation.
+  nsAutoString  key;
+  nsAutoString  value;
+  while (annotationsEnum->Next(key, value)) {
+    aKeyOut.Append(key);
+    aKeyOut.Append(value);
+  }
+
+  return NS_OK;
+}
+
+#if defined(MOZ_GECKO_PROFILER)
+void
+HangReports::AddHang(const Telemetry::ProcessedStack& aStack,
+                     uint32_t aDuration,
+                     int32_t aSystemUptime,
+                     int32_t aFirefoxUptime,
+                     HangAnnotationsPtr aAnnotations) {
+  // Append the new stack to the stack's circular queue.
+  size_t hangIndex = mStacks.AddStack(aStack);
+  // Append the hang info at the same index, in mHangInfo.
+  HangInfo info = { aDuration, aSystemUptime, aFirefoxUptime };
+  if (mHangInfo.size() < kMaxHangStacksKept) {
+    mHangInfo.push_back(info);
+  } else {
+    mHangInfo[hangIndex] = info;
+    // Remove any reference to the stack overwritten in the circular queue
+    // from the annotations.
+    PruneStackReferences(hangIndex);
+  }
+
+  if (!aAnnotations) {
+    return;
+  }
+
+  nsAutoString annotationsKey;
+  // Generate a key to index aAnnotations in the hash map.
+  nsresult rv = ComputeAnnotationsKey(aAnnotations, annotationsKey);
+  if (NS_FAILED(rv)) {
+    return;
+  }
+
+  AnnotationInfo* annotationsEntry = mAnnotationInfo.Get(annotationsKey);
+  if (annotationsEntry) {
+    // If the key is already in the hash map, append the index of the chrome hang
+    // to its indices.
+    annotationsEntry->mHangIndices.AppendElement(hangIndex);
+    return;
+  }
+
+  // If the key was not found, add the annotations to the hash map.
+  mAnnotationInfo.Put(annotationsKey, new AnnotationInfo(hangIndex, Move(aAnnotations)));
+}
+
+/**
+ * This function removes links to discarded chrome hangs stacks and prunes unused
+ * annotations.
+ */
+void
+HangReports::PruneStackReferences(const size_t aRemovedStackIndex) {
+  // We need to adjust the indices that link annotations to chrome hangs. Since we
+  // removed a stack, we must remove all references to it and prune annotations
+  // linked to no stacks.
+  for (auto iter = mAnnotationInfo.Iter(); !iter.Done(); iter.Next()) {
+    nsTArray<uint32_t>& stackIndices = iter.Data()->mHangIndices;
+    size_t toRemove = stackIndices.NoIndex;
+    for (size_t k = 0; k < stackIndices.Length(); k++) {
+      // Is this index referencing the removed stack?
+      if (stackIndices[k] == aRemovedStackIndex) {
+        toRemove = k;
+        break;
+      }
+    }
+
+    // Remove the index referencing the old stack from the annotation.
+    if (toRemove != stackIndices.NoIndex) {
+      stackIndices.RemoveElementAt(toRemove);
+    }
+
+    // If this annotation no longer references any stack, drop it.
+    if (!stackIndices.Length()) {
+      iter.Remove();
+    }
+  }
+}
+#endif
+
+size_t
+HangReports::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+  size_t n = 0;
+  n += mStacks.SizeOfExcludingThis();
+  // This is a crude approximation. See comment on
+  // CombinedStacks::SizeOfExcludingThis.
+  n += mHangInfo.capacity() * sizeof(HangInfo);
+  n += mAnnotationInfo.ShallowSizeOfExcludingThis(aMallocSizeOf);
+  n += mAnnotationInfo.Count() * sizeof(AnnotationInfo);
+  for (auto iter = mAnnotationInfo.ConstIter(); !iter.Done(); iter.Next()) {
+    n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+    n += iter.Data()->mAnnotations->SizeOfIncludingThis(aMallocSizeOf);
+  }
+  return n;
+}
+
+const CombinedStacks&
+HangReports::GetStacks() const {
+  return mStacks;
+}
+
+uint32_t
+HangReports::GetDuration(unsigned aIndex) const {
+  return mHangInfo[aIndex].mDuration;
+}
+
+int32_t
+HangReports::GetSystemUptime(unsigned aIndex) const {
+  return mHangInfo[aIndex].mSystemUptime;
+}
+
+int32_t
+HangReports::GetFirefoxUptime(unsigned aIndex) const {
+  return mHangInfo[aIndex].mFirefoxUptime;
+}
+
+const nsClassHashtable<nsStringHashKey, HangReports::AnnotationInfo>&
+HangReports::GetAnnotationInfo() const {
+  return mAnnotationInfo;
+}
+
+} // namespace Telemetry
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/HangReports.h
@@ -0,0 +1,90 @@
+/* -*-  Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef HangReports_h__
+#define HangReports_h__
+
+#include <vector>
+#include "mozilla/HangAnnotations.h"
+#include "ProcessedStack.h"
+#include "nsTArray.h"
+#include "nsString.h"
+#include "nsClassHashtable.h"
+#include "CombinedStacks.h"
+
+namespace mozilla {
+namespace Telemetry {
+
+nsresult
+ComputeAnnotationsKey(const HangMonitor::HangAnnotationsPtr& aAnnotations, nsAString& aKeyOut);
+
+class HangReports {
+public:
+  /**
+   * This struct encapsulates information for an individual ChromeHang annotation.
+   * mHangIndex is the index of the corresponding ChromeHang.
+   */
+  struct AnnotationInfo {
+    AnnotationInfo(uint32_t aHangIndex,
+                   HangMonitor::HangAnnotationsPtr aAnnotations)
+      : mAnnotations(Move(aAnnotations))
+    {
+      mHangIndices.AppendElement(aHangIndex);
+    }
+    AnnotationInfo(AnnotationInfo&& aOther)
+      : mHangIndices(aOther.mHangIndices)
+      , mAnnotations(Move(aOther.mAnnotations))
+    {}
+    ~AnnotationInfo() = default;
+    AnnotationInfo& operator=(AnnotationInfo&& aOther)
+    {
+      mHangIndices = aOther.mHangIndices;
+      mAnnotations = Move(aOther.mAnnotations);
+      return *this;
+    }
+    // To save memory, a single AnnotationInfo can be associated to multiple chrome
+    // hangs. The following array holds the index of each related chrome hang.
+    nsTArray<uint32_t> mHangIndices;
+    HangMonitor::HangAnnotationsPtr mAnnotations;
+
+  private:
+    // Force move constructor
+    AnnotationInfo(const AnnotationInfo& aOther) = delete;
+    void operator=(const AnnotationInfo& aOther) = delete;
+  };
+  size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+#if defined(MOZ_GECKO_PROFILER)
+  void AddHang(const Telemetry::ProcessedStack& aStack, uint32_t aDuration,
+               int32_t aSystemUptime, int32_t aFirefoxUptime,
+               HangMonitor::HangAnnotationsPtr aAnnotations);
+  void PruneStackReferences(const size_t aRemovedStackIndex);
+#endif
+  uint32_t GetDuration(unsigned aIndex) const;
+  int32_t GetSystemUptime(unsigned aIndex) const;
+  int32_t GetFirefoxUptime(unsigned aIndex) const;
+  const nsClassHashtable<nsStringHashKey, AnnotationInfo>& GetAnnotationInfo() const;
+  const CombinedStacks& GetStacks() const;
+private:
+  /**
+   * This struct encapsulates the data for an individual ChromeHang, excluding
+   * annotations.
+   */
+  struct HangInfo {
+    // Hang duration (in seconds)
+    uint32_t mDuration;
+    // System uptime (in minutes) at the time of the hang
+    int32_t mSystemUptime;
+    // Firefox uptime (in minutes) at the time of the hang
+    int32_t mFirefoxUptime;
+  };
+  std::vector<HangInfo> mHangInfo;
+  nsClassHashtable<nsStringHashKey, AnnotationInfo> mAnnotationInfo;
+  CombinedStacks mStacks;
+};
+
+} // namespace Telemetry
+} // namespace mozilla
+
+#endif // CombinedStacks_h__
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/KeyedStackCapturer.cpp
@@ -0,0 +1,174 @@
+/* -*- 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 "KeyedStackCapturer.h"
+#include "nsPrintfCString.h"
+#include "mozilla/StackWalk.h"
+#include "ProcessedStack.h"
+#include "jsapi.h"
+
+namespace {
+
+using namespace mozilla;
+using namespace Telemetry;
+
+#if defined(ENABLE_STACK_CAPTURE)
+
+/** Defines the size of the keyed stack dictionary. */
+const uint8_t kMaxKeyLength = 50;
+
+/** The maximum number of captured stacks that we're keeping. */
+const size_t kMaxCapturedStacksKept = 50;
+
+/**
+ * Checks if a single character of the key string is valid.
+ *
+ * @param aChar a character to validate.
+ * @return True, if the char is valid, False - otherwise.
+ */
+bool
+IsKeyCharValid(const char aChar)
+{
+  return (aChar >= 'A' && aChar <= 'Z')
+      || (aChar >= 'a' && aChar <= 'z')
+      || (aChar >= '0' && aChar <= '9')
+      || aChar == '-';
+}
+
+/**
+ * Checks if a given string is a valid telemetry key.
+ *
+ * @param aKey is the key string.
+ * @return True, if the key is valid, False - otherwise.
+ */
+bool
+IsKeyValid(const nsACString& aKey)
+{
+  // Check key length.
+  if (aKey.Length() > kMaxKeyLength) {
+    return false;
+  }
+
+  // Check key characters.
+  const char* cur = aKey.BeginReading();
+  const char* end = aKey.EndReading();
+
+  for (; cur < end; ++cur) {
+      if (!IsKeyCharValid(*cur)) {
+        return false;
+      }
+  }
+  return true;
+}
+
+} // anonymous namespace
+
+namespace mozilla {
+namespace Telemetry {
+
+KeyedStackCapturer::KeyedStackCapturer()
+  : mStackCapturerMutex("Telemetry::StackCapturerMutex")
+{}
+
+void KeyedStackCapturer::Capture(const nsACString& aKey) {
+  MutexAutoLock captureStackMutex(mStackCapturerMutex);
+
+  // Check if the key is ok.
+  if (!IsKeyValid(aKey)) {
+    NS_WARNING(nsPrintfCString(
+      "Invalid key is used to capture stack in telemetry: '%s'",
+      PromiseFlatCString(aKey).get()
+    ).get());
+    return;
+  }
+
+  // Trying to find and update the stack information.
+  StackFrequencyInfo* info = mStackInfos.Get(aKey);
+  if (info) {
+    // We already recorded this stack before, only increase the count.
+    info->mCount++;
+    return;
+  }
+
+  // Check if we have room for new captures.
+  if (mStackInfos.Count() >= kMaxCapturedStacksKept) {
+    // Addressed by Bug 1316793.
+    return;
+  }
+
+  // We haven't captured a stack for this key before, do it now.
+  // Note that this does a stackwalk and is an expensive operation.
+  std::vector<uintptr_t> rawStack;
+  auto callback = [](uint32_t, void* aPC, void*, void* aClosure) {
+    std::vector<uintptr_t>* stack =
+      static_cast<std::vector<uintptr_t>*>(aClosure);
+    stack->push_back(reinterpret_cast<uintptr_t>(aPC));
+  };
+  MozStackWalk(callback, /* skipFrames */ 0,
+              /* maxFrames */ 0, reinterpret_cast<void*>(&rawStack), 0, nullptr);
+  ProcessedStack stack = GetStackAndModules(rawStack);
+
+  // Store the new stack info.
+  size_t stackIndex = mStacks.AddStack(stack);
+  mStackInfos.Put(aKey, new StackFrequencyInfo(1, stackIndex));
+}
+
+NS_IMETHODIMP
+KeyedStackCapturer::ReflectCapturedStacks(JSContext *cx, JS::MutableHandle<JS::Value> ret)
+{
+  MutexAutoLock capturedStackMutex(mStackCapturerMutex);
+
+  // this adds the memoryMap and stacks properties.
+  JS::RootedObject fullReportObj(cx, CreateJSStackObject(cx, mStacks));
+  if (!fullReportObj) {
+    return NS_ERROR_FAILURE;
+  }
+
+  JS::RootedObject keysArray(cx, JS_NewArrayObject(cx, 0));
+  if (!keysArray) {
+    return NS_ERROR_FAILURE;
+  }
+
+  bool ok = JS_DefineProperty(cx, fullReportObj, "captures",
+                              keysArray, JSPROP_ENUMERATE);
+  if (!ok) {
+    return NS_ERROR_FAILURE;
+  }
+
+  size_t keyIndex = 0;
+  for (auto iter = mStackInfos.ConstIter(); !iter.Done(); iter.Next(), ++keyIndex) {
+    const StackFrequencyInfo* info = iter.Data();
+
+    JS::RootedObject infoArray(cx, JS_NewArrayObject(cx, 0));
+    if (!keysArray) {
+      return NS_ERROR_FAILURE;
+    }
+    JS::RootedString str(cx, JS_NewStringCopyZ(cx,
+                         PromiseFlatCString(iter.Key()).get()));
+    if (!str ||
+        !JS_DefineElement(cx, infoArray, 0, str, JSPROP_ENUMERATE) ||
+        !JS_DefineElement(cx, infoArray, 1, info->mIndex, JSPROP_ENUMERATE) ||
+        !JS_DefineElement(cx, infoArray, 2, info->mCount, JSPROP_ENUMERATE) ||
+        !JS_DefineElement(cx, keysArray, keyIndex, infoArray, JSPROP_ENUMERATE)) {
+      return NS_ERROR_FAILURE;
+    }
+  }
+
+  ret.setObject(*fullReportObj);
+  return NS_OK;
+}
+
+void
+KeyedStackCapturer::Clear()
+{
+  MutexAutoLock captureStackMutex(mStackCapturerMutex);
+  mStackInfos.Clear();
+  mStacks.Clear();
+}
+#endif
+
+} // namespace Telemetry
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/KeyedStackCapturer.h
@@ -0,0 +1,80 @@
+/* -*-  Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef KeyedStackCapturer_h__
+#define KeyedStackCapturer_h__
+
+#ifdef MOZ_GECKO_PROFILER
+#define ENABLE_STACK_CAPTURE
+
+#include "Telemetry.h"
+#include "nsString.h"
+#include "nsClassHashtable.h"
+#include "mozilla/Mutex.h"
+#include "CombinedStacks.h"
+
+class JSContext;
+
+namespace mozilla {
+namespace Telemetry {
+
+/**
+* Allows taking a snapshot of a call stack on demand. Captured stacks are
+* indexed by a string key in a hash table. The stack is only captured Once
+* for each key. Consequent captures with the same key result in incrementing
+* capture counter without re-capturing the stack.
+*/
+class KeyedStackCapturer
+{
+public:
+  KeyedStackCapturer()
+    : mStackCapturerMutex("Telemetry::StackCapturerMutex")
+  {}
+
+  /**
+   * Captures a stack for the given key.
+   */
+  void Capture(const nsACString& aKey);
+
+  /**
+   * Transforms captured stacks into a JS object.
+   */
+   NS_IMETHODIMP ReflectCapturedStacks(
+    JSContext *cx, JS::MutableHandle<JS::Value> ret);
+
+  /**
+   * Resets captured stacks and the information related to them.
+   */
+  void Clear();
+
+private:
+  /**
+   * Describes how often a stack was captured.
+   */
+  struct StackFrequencyInfo {
+    // A number of times the stack was captured.
+    uint32_t mCount;
+    // Index of the stack inside stacks array.
+    uint32_t mIndex;
+
+    StackFrequencyInfo(uint32_t aCount, uint32_t aIndex)
+      : mCount(aCount)
+      , mIndex(aIndex)
+    {}
+  };
+
+  typedef nsClassHashtable<nsCStringHashKey, StackFrequencyInfo> FrequencyInfoMapType;
+
+  FrequencyInfoMapType mStackInfos;
+  CombinedStacks mStacks;
+  Mutex mStackCapturerMutex;
+};
+
+} // namespace Telemetry
+} // namespace mozilla
+
+#endif // MOZ_GECKO_PROFILER
+
+#endif // KeyedStackCapturer_h__
--- a/toolkit/components/telemetry/Telemetry.cpp
+++ b/toolkit/components/telemetry/Telemetry.cpp
@@ -79,536 +79,39 @@
 #include "mozilla/Preferences.h"
 #include "mozilla/StaticPtr.h"
 #include "mozilla/IOInterposer.h"
 #include "mozilla/PoisonIOInterposer.h"
 #include "mozilla/StartupTimeline.h"
 #include "mozilla/HangMonitor.h"
 #include "nsNativeCharsetUtils.h"
 #include "nsProxyRelease.h"
+#include "HangReports.h"
 
 #if defined(MOZ_GECKO_PROFILER)
 #include "shared-libraries.h"
-#define ENABLE_STACK_CAPTURE
-#include "mozilla/StackWalk.h"
-#include "nsPrintfCString.h"
+#include "KeyedStackCapturer.h"
 #endif // MOZ_GECKO_PROFILER
 
 namespace {
 
 using namespace mozilla;
 using namespace mozilla::HangMonitor;
 using Telemetry::Common::AutoHashtable;
 using mozilla::dom::Promise;
 using mozilla::dom::AutoJSAPI;
 
-// The maximum number of chrome hangs stacks that we're keeping.
-const size_t kMaxChromeStacksKept = 50;
+using mozilla::Telemetry::HangReports;
+using mozilla::Telemetry::KeyedStackCapturer;
+using mozilla::Telemetry::CombinedStacks;
+using mozilla::Telemetry::ComputeAnnotationsKey;
+
 // The maximum depth of a single chrome hang stack.
 const size_t kMaxChromeStackDepth = 50;
 
-// This class is conceptually a list of ProcessedStack objects, but it represents them
-// more efficiently by keeping a single global list of modules.
-class CombinedStacks {
-public:
-  CombinedStacks() : mNextIndex(0) {}
-  typedef std::vector<Telemetry::ProcessedStack::Frame> Stack;
-  const Telemetry::ProcessedStack::Module& GetModule(unsigned aIndex) const;
-  size_t GetModuleCount() const;
-  const Stack& GetStack(unsigned aIndex) const;
-  size_t AddStack(const Telemetry::ProcessedStack& aStack);
-  size_t GetStackCount() const;
-  size_t SizeOfExcludingThis() const;
-
-#if defined(ENABLE_STACK_CAPTURE)
-  /** Clears the contents of vectors and resets the index. */
-  void Clear();
-#endif
-private:
-  std::vector<Telemetry::ProcessedStack::Module> mModules;
-  // A circular buffer to hold the stacks.
-  std::vector<Stack> mStacks;
-  // The index of the next buffer element to write to in mStacks.
-  size_t mNextIndex;
-};
-
-static JSObject *
-CreateJSStackObject(JSContext *cx, const CombinedStacks &stacks);
-
-size_t
-CombinedStacks::GetModuleCount() const {
-  return mModules.size();
-}
-
-const Telemetry::ProcessedStack::Module&
-CombinedStacks::GetModule(unsigned aIndex) const {
-  return mModules[aIndex];
-}
-
-size_t
-CombinedStacks::AddStack(const Telemetry::ProcessedStack& aStack) {
-  // Advance the indices of the circular queue holding the stacks.
-  size_t index = mNextIndex++ % kMaxChromeStacksKept;
-  // Grow the vector up to the maximum size, if needed.
-  if (mStacks.size() < kMaxChromeStacksKept) {
-    mStacks.resize(mStacks.size() + 1);
-  }
-  // Get a reference to the location holding the new stack.
-  CombinedStacks::Stack& adjustedStack = mStacks[index];
-  // If we're using an old stack to hold aStack, clear it.
-  adjustedStack.clear();
-
-  size_t stackSize = aStack.GetStackSize();
-  for (size_t i = 0; i < stackSize; ++i) {
-    const Telemetry::ProcessedStack::Frame& frame = aStack.GetFrame(i);
-    uint16_t modIndex;
-    if (frame.mModIndex == std::numeric_limits<uint16_t>::max()) {
-      modIndex = frame.mModIndex;
-    } else {
-      const Telemetry::ProcessedStack::Module& module =
-        aStack.GetModule(frame.mModIndex);
-      std::vector<Telemetry::ProcessedStack::Module>::iterator modIterator =
-        std::find(mModules.begin(), mModules.end(), module);
-      if (modIterator == mModules.end()) {
-        mModules.push_back(module);
-        modIndex = mModules.size() - 1;
-      } else {
-        modIndex = modIterator - mModules.begin();
-      }
-    }
-    Telemetry::ProcessedStack::Frame adjustedFrame = { frame.mOffset, modIndex };
-    adjustedStack.push_back(adjustedFrame);
-  }
-  return index;
-}
-
-const CombinedStacks::Stack&
-CombinedStacks::GetStack(unsigned aIndex) const {
-  return mStacks[aIndex];
-}
-
-size_t
-CombinedStacks::GetStackCount() const {
-  return mStacks.size();
-}
-
-size_t
-CombinedStacks::SizeOfExcludingThis() const {
-  // This is a crude approximation. We would like to do something like
-  // aMallocSizeOf(&mModules[0]), but on linux aMallocSizeOf will call
-  // malloc_usable_size which is only safe on the pointers returned by malloc.
-  // While it works on current libstdc++, it is better to be safe and not assume
-  // that &vec[0] points to one. We could use a custom allocator, but
-  // it doesn't seem worth it.
-  size_t n = 0;
-  n += mModules.capacity() * sizeof(Telemetry::ProcessedStack::Module);
-  n += mStacks.capacity() * sizeof(Stack);
-  for (const auto & s : mStacks) {
-    n += s.capacity() * sizeof(Telemetry::ProcessedStack::Frame);
-  }
-  return n;
-}
-
-// This utility function generates a string key that is used to index the annotations
-// in a hash map from |HangReports::AddHang|.
-nsresult
-ComputeAnnotationsKey(const HangAnnotationsPtr& aAnnotations, nsAString& aKeyOut)
-{
-  UniquePtr<HangAnnotations::Enumerator> annotationsEnum = aAnnotations->GetEnumerator();
-  if (!annotationsEnum) {
-    return NS_ERROR_FAILURE;
-  }
-
-  // Append all the attributes to the key, to uniquely identify this annotation.
-  nsAutoString  key;
-  nsAutoString  value;
-  while (annotationsEnum->Next(key, value)) {
-    aKeyOut.Append(key);
-    aKeyOut.Append(value);
-  }
-
-  return NS_OK;
-}
-
-#if defined(ENABLE_STACK_CAPTURE)
-void
-CombinedStacks::Clear() {
-  mNextIndex = 0;
-  mStacks.clear();
-  mModules.clear();
-}
-#endif
-
-class HangReports {
-public:
-  /**
-   * This struct encapsulates information for an individual ChromeHang annotation.
-   * mHangIndex is the index of the corresponding ChromeHang.
-   */
-  struct AnnotationInfo {
-    AnnotationInfo(uint32_t aHangIndex,
-                   HangAnnotationsPtr aAnnotations)
-      : mAnnotations(Move(aAnnotations))
-    {
-      mHangIndices.AppendElement(aHangIndex);
-    }
-    AnnotationInfo(AnnotationInfo&& aOther)
-      : mHangIndices(aOther.mHangIndices)
-      , mAnnotations(Move(aOther.mAnnotations))
-    {}
-    ~AnnotationInfo() = default;
-    AnnotationInfo& operator=(AnnotationInfo&& aOther)
-    {
-      mHangIndices = aOther.mHangIndices;
-      mAnnotations = Move(aOther.mAnnotations);
-      return *this;
-    }
-    // To save memory, a single AnnotationInfo can be associated to multiple chrome
-    // hangs. The following array holds the index of each related chrome hang.
-    nsTArray<uint32_t> mHangIndices;
-    HangAnnotationsPtr mAnnotations;
-
-  private:
-    // Force move constructor
-    AnnotationInfo(const AnnotationInfo& aOther) = delete;
-    void operator=(const AnnotationInfo& aOther) = delete;
-  };
-  size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
-#if defined(MOZ_GECKO_PROFILER)
-  void AddHang(const Telemetry::ProcessedStack& aStack, uint32_t aDuration,
-               int32_t aSystemUptime, int32_t aFirefoxUptime,
-               HangAnnotationsPtr aAnnotations);
-  void PruneStackReferences(const size_t aRemovedStackIndex);
-#endif
-  uint32_t GetDuration(unsigned aIndex) const;
-  int32_t GetSystemUptime(unsigned aIndex) const;
-  int32_t GetFirefoxUptime(unsigned aIndex) const;
-  const nsClassHashtable<nsStringHashKey, AnnotationInfo>& GetAnnotationInfo() const;
-  const CombinedStacks& GetStacks() const;
-private:
-  /**
-   * This struct encapsulates the data for an individual ChromeHang, excluding
-   * annotations.
-   */
-  struct HangInfo {
-    // Hang duration (in seconds)
-    uint32_t mDuration;
-    // System uptime (in minutes) at the time of the hang
-    int32_t mSystemUptime;
-    // Firefox uptime (in minutes) at the time of the hang
-    int32_t mFirefoxUptime;
-  };
-  std::vector<HangInfo> mHangInfo;
-  nsClassHashtable<nsStringHashKey, AnnotationInfo> mAnnotationInfo;
-  CombinedStacks mStacks;
-};
-
-#if defined(MOZ_GECKO_PROFILER)
-void
-HangReports::AddHang(const Telemetry::ProcessedStack& aStack,
-                     uint32_t aDuration,
-                     int32_t aSystemUptime,
-                     int32_t aFirefoxUptime,
-                     HangAnnotationsPtr aAnnotations) {
-  // Append the new stack to the stack's circular queue.
-  size_t hangIndex = mStacks.AddStack(aStack);
-  // Append the hang info at the same index, in mHangInfo.
-  HangInfo info = { aDuration, aSystemUptime, aFirefoxUptime };
-  if (mHangInfo.size() < kMaxChromeStacksKept) {
-    mHangInfo.push_back(info);
-  } else {
-    mHangInfo[hangIndex] = info;
-    // Remove any reference to the stack overwritten in the circular queue
-    // from the annotations.
-    PruneStackReferences(hangIndex);
-  }
-
-  if (!aAnnotations) {
-    return;
-  }
-
-  nsAutoString annotationsKey;
-  // Generate a key to index aAnnotations in the hash map.
-  nsresult rv = ComputeAnnotationsKey(aAnnotations, annotationsKey);
-  if (NS_FAILED(rv)) {
-    return;
-  }
-
-  AnnotationInfo* annotationsEntry = mAnnotationInfo.Get(annotationsKey);
-  if (annotationsEntry) {
-    // If the key is already in the hash map, append the index of the chrome hang
-    // to its indices.
-    annotationsEntry->mHangIndices.AppendElement(hangIndex);
-    return;
-  }
-
-  // If the key was not found, add the annotations to the hash map.
-  mAnnotationInfo.Put(annotationsKey, new AnnotationInfo(hangIndex, Move(aAnnotations)));
-}
-
-/**
- * This function removes links to discarded chrome hangs stacks and prunes unused
- * annotations.
- */
-void
-HangReports::PruneStackReferences(const size_t aRemovedStackIndex) {
-  // We need to adjust the indices that link annotations to chrome hangs. Since we
-  // removed a stack, we must remove all references to it and prune annotations
-  // linked to no stacks.
-  for (auto iter = mAnnotationInfo.Iter(); !iter.Done(); iter.Next()) {
-    nsTArray<uint32_t>& stackIndices = iter.Data()->mHangIndices;
-    size_t toRemove = stackIndices.NoIndex;
-    for (size_t k = 0; k < stackIndices.Length(); k++) {
-      // Is this index referencing the removed stack?
-      if (stackIndices[k] == aRemovedStackIndex) {
-        toRemove = k;
-        break;
-      }
-    }
-
-    // Remove the index referencing the old stack from the annotation.
-    if (toRemove != stackIndices.NoIndex) {
-      stackIndices.RemoveElementAt(toRemove);
-    }
-
-    // If this annotation no longer references any stack, drop it.
-    if (!stackIndices.Length()) {
-      iter.Remove();
-    }
-  }
-}
-#endif
-
-size_t
-HangReports::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
-  size_t n = 0;
-  n += mStacks.SizeOfExcludingThis();
-  // This is a crude approximation. See comment on
-  // CombinedStacks::SizeOfExcludingThis.
-  n += mHangInfo.capacity() * sizeof(HangInfo);
-  n += mAnnotationInfo.ShallowSizeOfExcludingThis(aMallocSizeOf);
-  n += mAnnotationInfo.Count() * sizeof(AnnotationInfo);
-  for (auto iter = mAnnotationInfo.ConstIter(); !iter.Done(); iter.Next()) {
-    n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
-    n += iter.Data()->mAnnotations->SizeOfIncludingThis(aMallocSizeOf);
-  }
-  return n;
-}
-
-const CombinedStacks&
-HangReports::GetStacks() const {
-  return mStacks;
-}
-
-uint32_t
-HangReports::GetDuration(unsigned aIndex) const {
-  return mHangInfo[aIndex].mDuration;
-}
-
-int32_t
-HangReports::GetSystemUptime(unsigned aIndex) const {
-  return mHangInfo[aIndex].mSystemUptime;
-}
-
-int32_t
-HangReports::GetFirefoxUptime(unsigned aIndex) const {
-  return mHangInfo[aIndex].mFirefoxUptime;
-}
-
-const nsClassHashtable<nsStringHashKey, HangReports::AnnotationInfo>&
-HangReports::GetAnnotationInfo() const {
-  return mAnnotationInfo;
-}
-
-#if defined(ENABLE_STACK_CAPTURE)
-
-const uint8_t kMaxKeyLength = 50;
-
-/**
- * Checks if a single character of the key string is valid.
- *
- * @param aChar a character to validate.
- * @return True, if the char is valid, False - otherwise.
- */
-bool
-IsKeyCharValid(const char aChar)
-{
-  return (aChar >= 'A' && aChar <= 'Z')
-      || (aChar >= 'a' && aChar <= 'z')
-      || (aChar >= '0' && aChar <= '9')
-      || aChar == '-';
-}
-
-/**
- * Checks if a given string is a valid telemetry key.
- *
- * @param aKey is the key string.
- * @return True, if the key is valid, False - otherwise.
- */
-bool
-IsKeyValid(const nsACString& aKey)
-{
-  // Check key length.
-  if (aKey.Length() > kMaxKeyLength) {
-    return false;
-  }
-
-  // Check key characters.
-  const char* cur = aKey.BeginReading();
-  const char* end = aKey.EndReading();
-
-  for (; cur < end; ++cur) {
-      if (!IsKeyCharValid(*cur)) {
-        return false;
-      }
-  }
-  return true;
-}
-
-/**
- * Allows taking a snapshot of a call stack on demand. Captured stacks are
- * indexed by a string key in a hash table. The stack is only captured Once
- * for each key. Consequent captures with the same key result in incrementing
- * capture counter without re-capturing the stack.
- */
-class KeyedStackCapturer {
-public:
-  KeyedStackCapturer();
-
-  void Capture(const nsACString& aKey);
-  NS_IMETHODIMP ReflectCapturedStacks(JSContext *cx, JS::MutableHandle<JS::Value> ret);
-
-  /**
-   * Resets captured stacks and the information related to them.
-   */
-  void Clear();
-private:
-  /**
-   * Describes how often a stack was captured.
-   */
-  struct StackFrequencyInfo {
-    // A number of times the stack was captured.
-    uint32_t mCount;
-    // Index of the stack inside stacks array.
-    uint32_t mIndex;
-
-    StackFrequencyInfo(uint32_t aCount, uint32_t aIndex)
-      : mCount(aCount)
-      , mIndex(aIndex)
-    {}
-  };
-
-  typedef nsClassHashtable<nsCStringHashKey, StackFrequencyInfo> FrequencyInfoMapType;
-
-  FrequencyInfoMapType mStackInfos;
-  CombinedStacks mStacks;
-  Mutex mStackCapturerMutex;
-};
-
-KeyedStackCapturer::KeyedStackCapturer()
-  : mStackCapturerMutex("Telemetry::StackCapturerMutex")
-{}
-
-void KeyedStackCapturer::Capture(const nsACString& aKey) {
-  MutexAutoLock captureStackMutex(mStackCapturerMutex);
-
-  // Check if the key is ok.
-  if (!IsKeyValid(aKey)) {
-    NS_WARNING(nsPrintfCString(
-      "Invalid key is used to capture stack in telemetry: '%s'",
-      PromiseFlatCString(aKey).get()
-    ).get());
-    return;
-  }
-
-  // Trying to find and update the stack information.
-  StackFrequencyInfo* info = mStackInfos.Get(aKey);
-  if (info) {
-    // We already recorded this stack before, only increase the count.
-    info->mCount++;
-    return;
-  }
-
-  // Check if we have room for new captures.
-  if (mStackInfos.Count() >= kMaxChromeStacksKept) {
-    // Addressed by Bug 1316793.
-    return;
-  }
-
-  // We haven't captured a stack for this key before, do it now.
-  // Note that this does a stackwalk and is an expensive operation.
-  std::vector<uintptr_t> rawStack;
-  auto callback = [](uint32_t, void* aPC, void*, void* aClosure) {
-    std::vector<uintptr_t>* stack =
-      static_cast<std::vector<uintptr_t>*>(aClosure);
-    stack->push_back(reinterpret_cast<uintptr_t>(aPC));
-  };
-  MozStackWalk(callback, /* skipFrames */ 0,
-              /* maxFrames */ 0, reinterpret_cast<void*>(&rawStack), 0, nullptr);
-  Telemetry::ProcessedStack stack = Telemetry::GetStackAndModules(rawStack);
-
-  // Store the new stack info.
-  size_t stackIndex = mStacks.AddStack(stack);
-  mStackInfos.Put(aKey, new StackFrequencyInfo(1, stackIndex));
-}
-
-NS_IMETHODIMP
-KeyedStackCapturer::ReflectCapturedStacks(JSContext *cx, JS::MutableHandle<JS::Value> ret)
-{
-  MutexAutoLock capturedStackMutex(mStackCapturerMutex);
-
-  // this adds the memoryMap and stacks properties.
-  JS::RootedObject fullReportObj(cx, CreateJSStackObject(cx, mStacks));
-  if (!fullReportObj) {
-    return NS_ERROR_FAILURE;
-  }
-
-  JS::RootedObject keysArray(cx, JS_NewArrayObject(cx, 0));
-  if (!keysArray) {
-    return NS_ERROR_FAILURE;
-  }
-
-  bool ok = JS_DefineProperty(cx, fullReportObj, "captures",
-                              keysArray, JSPROP_ENUMERATE);
-  if (!ok) {
-    return NS_ERROR_FAILURE;
-  }
-
-  size_t keyIndex = 0;
-  for (auto iter = mStackInfos.ConstIter(); !iter.Done(); iter.Next(), ++keyIndex) {
-    const StackFrequencyInfo* info = iter.Data();
-
-    JS::RootedObject infoArray(cx, JS_NewArrayObject(cx, 0));
-    if (!keysArray) {
-      return NS_ERROR_FAILURE;
-    }
-    JS::RootedString str(cx, JS_NewStringCopyZ(cx,
-                         PromiseFlatCString(iter.Key()).get()));
-    if (!str ||
-        !JS_DefineElement(cx, infoArray, 0, str, JSPROP_ENUMERATE) ||
-        !JS_DefineElement(cx, infoArray, 1, info->mIndex, JSPROP_ENUMERATE) ||
-        !JS_DefineElement(cx, infoArray, 2, info->mCount, JSPROP_ENUMERATE) ||
-        !JS_DefineElement(cx, keysArray, keyIndex, infoArray, JSPROP_ENUMERATE)) {
-      return NS_ERROR_FAILURE;
-    }
-  }
-
-  ret.setObject(*fullReportObj);
-  return NS_OK;
-}
-
-void
-KeyedStackCapturer::Clear()
-{
-  MutexAutoLock captureStackMutex(mStackCapturerMutex);
-  mStackInfos.Clear();
-  mStacks.Clear();
-}
-#endif
-
 /**
  * IOInterposeObserver recording statistics of main-thread I/O during execution,
  * aimed at consumption by TelemetryImpl
  */
 class TelemetryIOInterposeObserver : public IOInterposeObserver
 {
   /** File-level statistics structure */
   struct FileStats {
@@ -1535,110 +1038,16 @@ TelemetryImpl::SnapshotCapturedStacks(bo
     mStackCapturer.Clear();
   }
   return rv;
 #else
   return NS_OK;
 #endif
 }
 
-static JSObject *
-CreateJSStackObject(JSContext *cx, const CombinedStacks &stacks) {
-  JS::Rooted<JSObject*> ret(cx, JS_NewPlainObject(cx));
-  if (!ret) {
-    return nullptr;
-  }
-
-  JS::Rooted<JSObject*> moduleArray(cx, JS_NewArrayObject(cx, 0));
-  if (!moduleArray) {
-    return nullptr;
-  }
-  bool ok = JS_DefineProperty(cx, ret, "memoryMap", moduleArray,
-                              JSPROP_ENUMERATE);
-  if (!ok) {
-    return nullptr;
-  }
-
-  const size_t moduleCount = stacks.GetModuleCount();
-  for (size_t moduleIndex = 0; moduleIndex < moduleCount; ++moduleIndex) {
-    // Current module
-    const Telemetry::ProcessedStack::Module& module =
-      stacks.GetModule(moduleIndex);
-
-    JS::Rooted<JSObject*> moduleInfoArray(cx, JS_NewArrayObject(cx, 0));
-    if (!moduleInfoArray) {
-      return nullptr;
-    }
-    if (!JS_DefineElement(cx, moduleArray, moduleIndex, moduleInfoArray,
-                          JSPROP_ENUMERATE)) {
-      return nullptr;
-    }
-
-    unsigned index = 0;
-
-    // Module name
-    JS::Rooted<JSString*> str(cx, JS_NewUCStringCopyZ(cx, module.mName.get()));
-    if (!str || !JS_DefineElement(cx, moduleInfoArray, index++, str, JSPROP_ENUMERATE)) {
-      return nullptr;
-    }
-
-    // Module breakpad identifier
-    JS::Rooted<JSString*> id(cx, JS_NewStringCopyZ(cx, module.mBreakpadId.c_str()));
-    if (!id || !JS_DefineElement(cx, moduleInfoArray, index++, id, JSPROP_ENUMERATE)) {
-      return nullptr;
-    }
-  }
-
-  JS::Rooted<JSObject*> reportArray(cx, JS_NewArrayObject(cx, 0));
-  if (!reportArray) {
-    return nullptr;
-  }
-  ok = JS_DefineProperty(cx, ret, "stacks", reportArray, JSPROP_ENUMERATE);
-  if (!ok) {
-    return nullptr;
-  }
-
-  const size_t length = stacks.GetStackCount();
-  for (size_t i = 0; i < length; ++i) {
-    // Represent call stack PCs as (module index, offset) pairs.
-    JS::Rooted<JSObject*> pcArray(cx, JS_NewArrayObject(cx, 0));
-    if (!pcArray) {
-      return nullptr;
-    }
-
-    if (!JS_DefineElement(cx, reportArray, i, pcArray, JSPROP_ENUMERATE)) {
-      return nullptr;
-    }
-
-    const CombinedStacks::Stack& stack = stacks.GetStack(i);
-    const uint32_t pcCount = stack.size();
-    for (size_t pcIndex = 0; pcIndex < pcCount; ++pcIndex) {
-      const Telemetry::ProcessedStack::Frame& frame = stack[pcIndex];
-      JS::Rooted<JSObject*> framePair(cx, JS_NewArrayObject(cx, 0));
-      if (!framePair) {
-        return nullptr;
-      }
-      int modIndex = (std::numeric_limits<uint16_t>::max() == frame.mModIndex) ?
-        -1 : frame.mModIndex;
-      if (!JS_DefineElement(cx, framePair, 0, modIndex, JSPROP_ENUMERATE)) {
-        return nullptr;
-      }
-      if (!JS_DefineElement(cx, framePair, 1, static_cast<double>(frame.mOffset),
-                            JSPROP_ENUMERATE)) {
-        return nullptr;
-      }
-      if (!JS_DefineElement(cx, pcArray, pcIndex, framePair, JSPROP_ENUMERATE)) {
-        return nullptr;
-      }
-    }
-  }
-
-  return ret;
-}
-
 #if defined(MOZ_GECKO_PROFILER)
 class GetLoadedModulesResultRunnable final : public Runnable
 {
   nsMainThreadPtrHandle<Promise> mPromise;
   SharedLibraryInfo mRawModules;
   nsCOMPtr<nsIThread> mWorkerThread;
 
 public:
--- a/toolkit/components/telemetry/moz.build
+++ b/toolkit/components/telemetry/moz.build
@@ -47,18 +47,21 @@ EXPORTS.mozilla += [
     'ipc/TelemetryComms.h',
     'ipc/TelemetryIPC.h',
     'ProcessedStack.h',
     'Telemetry.h',
     'ThreadHangStats.h',
 ]
 
 SOURCES += [
+    'CombinedStacks.cpp',
+    'HangReports.cpp',
     'ipc/TelemetryIPC.cpp',
     'ipc/TelemetryIPCAccumulator.cpp',
+    'KeyedStackCapturer.cpp',
     'Telemetry.cpp',
     'TelemetryCommon.cpp',
     'TelemetryEvent.cpp',
     'TelemetryHistogram.cpp',
     'TelemetryScalar.cpp',
     'WebrtcTelemetry.cpp',
 ]