Bug 1154115 - Rewrite profiler JSON streaming. (r=mstange, a=sledru)
authorShu-yu Guo <shu@rfrn.org>
Mon, 11 May 2015 14:16:44 -0700
changeset 273290 abc21b1b28adaf480fca7aa7a9d9339c54056443
parent 273289 1d73cc5907f1dfb2a777c857b85aa08d61af3f84
child 273291 ea3d70cbe275f099dbc4e87e726c47827a6324ef
push id4830
push userjlund@mozilla.com
push dateMon, 29 Jun 2015 20:18:48 +0000
treeherdermozilla-beta@4c2175bb0420 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmstange, sledru
bugs1154115
milestone40.0a2
Bug 1154115 - Rewrite profiler JSON streaming. (r=mstange, a=sledru)
js/public/ProfilingFrameIterator.h
js/src/devtools/rootAnalysis/annotations.js
js/src/shell/js.cpp
mfbt/JSONWriter.h
tools/profiler/JSStreamWriter.cpp
tools/profiler/JSStreamWriter.h
tools/profiler/LulMain.h
tools/profiler/ProfileEntry.cpp
tools/profiler/ProfileEntry.h
tools/profiler/ProfileJSONWriter.cpp
tools/profiler/ProfileJSONWriter.h
tools/profiler/ProfilerBacktrace.cpp
tools/profiler/ProfilerBacktrace.h
tools/profiler/ProfilerMarkers.cpp
tools/profiler/ProfilerMarkers.h
tools/profiler/PseudoStack.h
tools/profiler/SyncProfile.cpp
tools/profiler/SyncProfile.h
tools/profiler/TableTicker.cpp
tools/profiler/TableTicker.h
tools/profiler/moz.build
tools/profiler/platform.cpp
tools/profiler/tests/gtest/JSStreamWriterTest.cpp
tools/profiler/tests/gtest/moz.build
--- a/js/public/ProfilingFrameIterator.h
+++ b/js/public/ProfilingFrameIterator.h
@@ -147,18 +147,18 @@ UpdateJSRuntimeProfilerSampleBufferGen(J
                                        uint32_t lapCount);
 
 struct ForEachProfiledFrameOp
 {
     // A handle to the underlying JitcodeGlobalEntry, so as to avoid repeated
     // lookups on JitcodeGlobalTable.
     class MOZ_STACK_CLASS FrameHandle
     {
-        friend JS_PUBLIC_API(void) JS::ForEachProfiledFrame(JSRuntime* rt, void* addr,
-                                                            ForEachProfiledFrameOp& op);
+        friend JS_PUBLIC_API(void) ForEachProfiledFrame(JSRuntime* rt, void* addr,
+                                                        ForEachProfiledFrameOp& op);
 
         JSRuntime* rt_;
         js::jit::JitcodeGlobalEntry& entry_;
         void* addr_;
         void* canonicalAddr_;
         const char* label_;
         uint32_t depth_;
         mozilla::Maybe<uint8_t> optsIndex_;
--- a/js/src/devtools/rootAnalysis/annotations.js
+++ b/js/src/devtools/rootAnalysis/annotations.js
@@ -77,17 +77,17 @@ var ignoreCallees = {
     "JSRuntime.destroyPrincipals" : true,
     "icu_50::UObject.__deleting_dtor" : true, // destructors in ICU code can't cause GC
     "mozilla::CycleCollectedJSRuntime.DescribeCustomObjects" : true, // During tracing, cannot GC.
     "mozilla::CycleCollectedJSRuntime.NoteCustomGCThingXPCOMChildren" : true, // During tracing, cannot GC.
     "PLDHashTableOps.hashKey" : true,
     "z_stream_s.zfree" : true,
     "GrGLInterface.fCallback" : true,
     "std::strstreambuf._M_alloc_fun" : true,
-    "std::strstreambuf._M_free_fun" : true,
+    "std::strstreambuf._M_free_fun" : true
 };
 
 function fieldCallCannotGC(csu, fullfield)
 {
     if (csu in ignoreClasses)
         return true;
     if (fullfield in ignoreCallees)
         return true;
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -4427,16 +4427,19 @@ ReflectTrackedOptimizations(JSContext* c
     }
 
     RootedFunction fun(cx, &args[0].toObject().as<JSFunction>());
     if (!fun->hasScript() || !fun->nonLazyScript()->hasIonScript()) {
         args.rval().setNull();
         return true;
     }
 
+    // Suppress GC for the unrooted JitcodeGlobalEntry below.
+    gc::AutoSuppressGC suppress(cx);
+
     jit::JitcodeGlobalTable* table = rt->jitRuntime()->getJitcodeGlobalTable();
     jit::JitcodeGlobalEntry entry;
     jit::IonScript* ion = fun->nonLazyScript()->ionScript();
     table->lookupInfallible(ion->method()->raw(), &entry, rt);
 
     if (!entry.hasTrackedOptimizations()) {
         JSObject* obj = JS_NewPlainObject(cx);
         if (!obj)
--- a/mfbt/JSONWriter.h
+++ b/mfbt/JSONWriter.h
@@ -233,17 +233,17 @@ public:
   // specified. If a collection is printed in single-line style, every nested
   // collection within it is also printed in single-line style, even if
   // multi-line style is requested.
   enum CollectionStyle {
     MultiLineStyle,   // the default
     SingleLineStyle
   };
 
-private:
+protected:
   const UniquePtr<JSONWriteFunc> mWriter;
   Vector<bool, 8> mNeedComma;     // do we need a comma at depth N?
   Vector<bool, 8> mNeedNewlines;  // do we need newlines at depth N?
   size_t mDepth;                  // the current nesting depth
 
   void Indent()
   {
     for (size_t i = 0; i < mDepth; i++) {
deleted file mode 100644
--- a/tools/profiler/JSStreamWriter.cpp
+++ /dev/null
@@ -1,225 +0,0 @@
-/* -*- 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/. */
-
-#include "JSStreamWriter.h"
-
-#include "mozilla/ArrayUtils.h" // for ArrayLength
-#include "nsDataHashtable.h"
-#include "nsString.h"
-#include "nsTArray.h"
-#include "nsUTF8Utils.h"
-
-#if defined(_MSC_VER) && _MSC_VER < 1900
- #define snprintf _snprintf
-#endif
-
-#define ARRAY (void*)1
-#define OBJECT (void*)2
-
-// Escape a UTF8 string to a stream. When an illegal encoding
-// is found it will insert "INVALID" and the function will return.
-static void EscapeToStream(std::ostream& stream, const char* str) {
-  stream << "\"";
-
-  size_t len = strlen(str);
-  const char* end = &str[len];
-  while (str < end) {
-    bool err;
-    const char* utf8CharStart = str;
-    uint32_t ucs4Char = UTF8CharEnumerator::NextChar(&str, end, &err);
-
-    if (err) {
-      // Encoding error
-      stream << "INVALID\"";
-      return;
-    }
-
-    // See http://www.ietf.org/rfc/rfc4627.txt?number=4627
-    // characters that must be escaped: quotation mark,
-    // reverse solidus, and the control characters
-    // (U+0000 through U+001F).
-    if (ucs4Char == '\"') {
-      stream << "\\\"";
-    } else if (ucs4Char == '\\') {
-      stream << "\\\\";
-    } else if (ucs4Char > 0xFF) {
-      char16_t chr[2];
-      ConvertUTF8toUTF16 encoder(chr);
-      encoder.write(utf8CharStart, uint32_t(str-utf8CharStart));
-      char escChar[13];
-      snprintf(escChar, mozilla::ArrayLength(escChar), "\\u%04X\\u%04X", chr[0], chr[1]);
-      stream << escChar;
-    } else if (ucs4Char < 0x1F || ucs4Char > 0xFF) {
-      char escChar[7];
-      snprintf(escChar, mozilla::ArrayLength(escChar), "\\u%04X", ucs4Char);
-      stream << escChar;
-    } else {
-      stream << char(ucs4Char);
-    }
-  }
-  stream << "\"";
-}
-
-JSStreamWriter::JSStreamWriter(std::ostream& aStream)
-  : mStream(aStream)
-  , mNeedsComma(false)
-  , mNeedsName(false)
-{ }
-
-JSStreamWriter::~JSStreamWriter()
-{
-  MOZ_ASSERT(mStack.GetSize() == 0);
-}
-
-void
-JSStreamWriter::BeginObject()
-{
-  MOZ_ASSERT(!mNeedsName);
-  if (mNeedsComma && mStack.Peek() == ARRAY) {
-    mStream << ",";
-  }
-  mStream << "{";
-  mNeedsComma = false;
-  mNeedsName = true;
-  mStack.Push(OBJECT);
-}
-
-void
-JSStreamWriter::EndObject()
-{
-  MOZ_ASSERT(mStack.Peek() == OBJECT);
-  mStream << "}";
-  mNeedsComma = true;
-  mNeedsName = false;
-  mStack.Pop();
-  if (mStack.GetSize() > 0 && mStack.Peek() == OBJECT) {
-    mNeedsName = true;
-  }
-}
-
-void
-JSStreamWriter::BeginArray()
-{
-  MOZ_ASSERT(!mNeedsName);
-  if (mNeedsComma && mStack.Peek() == ARRAY) {
-    mStream << ",";
-  }
-  mStream << "[";
-  mNeedsComma = false;
-  mStack.Push(ARRAY);
-}
-
-void
-JSStreamWriter::EndArray()
-{
-  MOZ_ASSERT(!mNeedsName);
-  MOZ_ASSERT(mStack.Peek() == ARRAY);
-  mStream << "]";
-  mNeedsComma = true;
-  mStack.Pop();
-  if (mStack.GetSize() > 0 && mStack.Peek() == OBJECT) {
-    mNeedsName = true;
-  }
-}
-
-void
-JSStreamWriter::BeginBareList()
-{
-  MOZ_ASSERT(!mNeedsName);
-  MOZ_ASSERT(mStack.GetSize() == 0);
-  mNeedsComma = false;
-  mStack.Push(ARRAY);
-}
-
-void
-JSStreamWriter::EndBareList()
-{
-  MOZ_ASSERT(!mNeedsName);
-  MOZ_ASSERT(mStack.Peek() == ARRAY);
-  mNeedsComma = true;
-  mStack.Pop();
-  MOZ_ASSERT(mStack.GetSize() == 0);
-}
-
-void
-JSStreamWriter::SpliceArrayElements(const char* aElements)
-{
-  MOZ_ASSERT(!mNeedsName);
-  MOZ_ASSERT(mStack.Peek() == ARRAY);
-  if (mNeedsComma) {
-    mStream << ",";
-  }
-  mStream << aElements;
-  mNeedsComma = true;
-}
-
-void
-JSStreamWriter::Name(const char *aName)
-{
-  MOZ_ASSERT(mNeedsName);
-  if (mNeedsComma && mStack.Peek() == OBJECT) {
-    mStream << ",";
-  }
-  EscapeToStream(mStream, aName);
-  mStream << ":";
-  mNeedsName = false;
-}
-
-void
-JSStreamWriter::Value(int aValue)
-{
-  MOZ_ASSERT(!mNeedsName);
-  if (mNeedsComma && mStack.Peek() == ARRAY) {
-    mStream << ",";
-  }
-  mStream << aValue;
-  mNeedsComma = true;
-  if (mStack.Peek() == OBJECT) {
-    mNeedsName = true;
-  }
-}
-
-void
-JSStreamWriter::Value(unsigned aValue)
-{
-  MOZ_ASSERT(!mNeedsName);
-  if (mNeedsComma && mStack.Peek() == ARRAY) {
-    mStream << ",";
-  }
-  mStream << aValue;
-  mNeedsComma = true;
-  if (mStack.Peek() == OBJECT) {
-    mNeedsName = true;
-  }
-}
-
-void
-JSStreamWriter::Value(double aValue)
-{
-  MOZ_ASSERT(!mNeedsName);
-  if (mNeedsComma && mStack.Peek() == ARRAY) {
-    mStream << ",";
-  }
-  mStream.precision(18);
-  mStream << aValue;
-  mNeedsComma = true;
-  if (mStack.Peek() == OBJECT) {
-    mNeedsName = true;
-  }
-}
-
-void
-JSStreamWriter::Value(const char *aValue)
-{
-  MOZ_ASSERT(!mNeedsName);
-  if (mNeedsComma && mStack.Peek() == ARRAY) {
-    mStream << ",";
-  }
-  EscapeToStream(mStream, aValue);
-  mNeedsComma = true;
-  if (mStack.Peek() == OBJECT) {
-    mNeedsName = true;
-  }
-}
deleted file mode 100644
--- a/tools/profiler/JSStreamWriter.h
+++ /dev/null
@@ -1,70 +0,0 @@
-/* -*- 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 JSSTREAMWRITER_H
-#define JSSTREAMWRITER_H
-
-#include <ostream>
-#include <stdlib.h>
-#include "nsDeque.h"
-
-class JSStreamWriter
-{
-public:
-  explicit JSStreamWriter(std::ostream& aStream);
-  ~JSStreamWriter();
-
-  void BeginObject();
-  void EndObject();
-  void BeginArray();
-  void EndArray();
-
-  // Begin or end an array without emitting surrounding brackets. This is used
-  // for saving streamed samples and markers on JS shutdown, as some JS
-  // samples cannot be symbolicated without a JSRuntime.
-  void BeginBareList();
-  void EndBareList();
-
-  // Splices aElements into an open array context. Used in conjunction with
-  // previously saved array elements from {Begin,End}BareList above.
-  void SpliceArrayElements(const char* aElements);
-
-  void Name(const char *name);
-  void Value(int value);
-  void Value(unsigned value);
-  void Value(double value);
-  void Value(const char *value, size_t valueLength);
-  void Value(const char *value);
-  template <typename T>
-  void NameValue(const char *aName, T aValue)
-  {
-    Name(aName);
-    Value(aValue);
-  }
-
-private:
-  std::ostream& mStream;
-  bool mNeedsComma;
-  bool mNeedsName;
-
-  nsDeque mStack;
-
-  // This class can't be copied
-  JSStreamWriter(const JSStreamWriter&);
-  JSStreamWriter& operator=(const JSStreamWriter&);
-
-  void* operator new(size_t);
-  void* operator new[](size_t);
-  void operator delete(void*) {
-    // Since JSStreamWriter has a virtual destructor the compiler
-    // has to provide a destructor in the object file that will call
-    // operate delete in case there is a derived class since its
-    // destructor won't know how to free this instance.
-    abort();
-  }
-  void operator delete[](void*);
-};
-
-#endif
--- a/tools/profiler/LulMain.h
+++ b/tools/profiler/LulMain.h
@@ -3,16 +3,17 @@
 /* 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 LulMain_h
 #define LulMain_h
 
 #include "LulPlatformMacros.h"
+#include "mozilla/Atomics.h"
 
 // LUL: A Lightweight Unwind Library.
 // This file provides the end-user (external) interface for LUL.
 
 // Some comments about naming in the implementation.  These are safe
 // to ignore if you are merely using LUL, but are important if you
 // hack on its internals.
 //
--- a/tools/profiler/ProfileEntry.cpp
+++ b/tools/profiler/ProfileEntry.cpp
@@ -1,39 +1,40 @@
 /* -*- 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/. */
 
 #include <ostream>
-#include <sstream>
 #include "platform.h"
 #include "nsThreadUtils.h"
 #include "nsXULAppAPI.h"
+#include "mozilla/HashFunctions.h"
 
 // JS
 #include "jsapi.h"
 #include "jsfriendapi.h"
-#include "js/ProfilingFrameIterator.h"
 #include "js/TrackedOptimizationInfo.h"
 
 // JSON
-#include "JSStreamWriter.h"
+#include "ProfileJSONWriter.h"
 
 // Self
 #include "ProfileEntry.h"
 
 #if defined(_MSC_VER) && _MSC_VER < 1900
  #define snprintf _snprintf
 #endif
 
 using mozilla::MakeUnique;
+using mozilla::UniquePtr;
 using mozilla::Maybe;
 using mozilla::Some;
 using mozilla::Nothing;
+using mozilla::JSONWriter;
 
 
 ////////////////////////////////////////////////////////////////////////
 // BEGIN ProfileEntry
 
 ProfileEntry::ProfileEntry()
   : mTagData(nullptr)
   , mTagName(0)
@@ -218,362 +219,620 @@ void ProfileBuffer::IterateTagsForThread
     if (currentThreadID == aThreadId) {
       aCallback(entry, tagStringData);
     }
 
     readPos = (readPos + incBy) % mEntrySize;
   }
 }
 
+class JSONSchemaWriter
+{
+  JSONWriter& mWriter;
+  uint32_t mIndex;
+
+public:
+  explicit JSONSchemaWriter(JSONWriter& aWriter)
+   : mWriter(aWriter)
+   , mIndex(0)
+  {
+    aWriter.StartObjectProperty("schema");
+  }
+
+  void WriteField(const char* aName) {
+    mWriter.IntProperty(aName, mIndex++);
+  }
+
+  ~JSONSchemaWriter() {
+    mWriter.EndObject();
+  }
+};
+
 class StreamOptimizationTypeInfoOp : public JS::ForEachTrackedOptimizationTypeInfoOp
 {
-  JSStreamWriter& mWriter;
+  JSONWriter& mWriter;
+  UniqueJSONStrings& mUniqueStrings;
   bool mStartedTypeList;
 
 public:
-  explicit StreamOptimizationTypeInfoOp(JSStreamWriter& b)
-    : mWriter(b)
+  StreamOptimizationTypeInfoOp(JSONWriter& aWriter, UniqueJSONStrings& aUniqueStrings)
+    : mWriter(aWriter)
+    , mUniqueStrings(aUniqueStrings)
     , mStartedTypeList(false)
   { }
 
   void readType(const char* keyedBy, const char* name,
                 const char* location, Maybe<unsigned> lineno) override {
     if (!mStartedTypeList) {
       mStartedTypeList = true;
-      mWriter.BeginObject();
-        mWriter.Name("types");
-        mWriter.BeginArray();
+      mWriter.StartObjectElement();
+      mWriter.StartArrayProperty("typeset");
     }
 
-    mWriter.BeginObject();
-      mWriter.NameValue("keyedBy", keyedBy);
+    mWriter.StartObjectElement();
+    {
+      mUniqueStrings.WriteProperty(mWriter, "keyedBy", keyedBy);
       if (name) {
-        mWriter.NameValue("name", name);
+        mUniqueStrings.WriteProperty(mWriter, "name", name);
       }
       if (location) {
-        mWriter.NameValue("location", location);
+        mUniqueStrings.WriteProperty(mWriter, "location", location);
       }
       if (lineno.isSome()) {
-        mWriter.NameValue("line", *lineno);
+        mWriter.IntProperty("line", *lineno);
       }
+    }
     mWriter.EndObject();
   }
 
   void operator()(JS::TrackedTypeSite site, const char* mirType) override {
     if (mStartedTypeList) {
       mWriter.EndArray();
       mStartedTypeList = false;
     } else {
-      mWriter.BeginObject();
+      mWriter.StartObjectElement();
     }
 
-      mWriter.NameValue("site", JS::TrackedTypeSiteString(site));
-      mWriter.NameValue("mirType", mirType);
+    {
+      mUniqueStrings.WriteProperty(mWriter, "site", JS::TrackedTypeSiteString(site));
+      mUniqueStrings.WriteProperty(mWriter, "mirType", mirType);
+    }
     mWriter.EndObject();
   }
 };
 
 class StreamOptimizationAttemptsOp : public JS::ForEachTrackedOptimizationAttemptOp
 {
-  JSStreamWriter& mWriter;
+  JSONWriter& mWriter;
+  UniqueJSONStrings& mUniqueStrings;
 
 public:
-  explicit StreamOptimizationAttemptsOp(JSStreamWriter& b)
-    : mWriter(b)
+  StreamOptimizationAttemptsOp(JSONWriter& aWriter, UniqueJSONStrings& aUniqueStrings)
+    : mWriter(aWriter),
+      mUniqueStrings(aUniqueStrings)
   { }
 
   void operator()(JS::TrackedStrategy strategy, JS::TrackedOutcome outcome) override {
-    mWriter.BeginObject();
-      // Stringify the reasons for now; could stream enum values in the future
-      // to save space.
-      mWriter.NameValue("strategy", JS::TrackedStrategyString(strategy));
-      mWriter.NameValue("outcome", JS::TrackedOutcomeString(outcome));
-    mWriter.EndObject();
+    // Schema:
+    //   [strategy, outcome]
+
+    mWriter.StartArrayElement();
+    {
+      mUniqueStrings.WriteElement(mWriter, JS::TrackedStrategyString(strategy));
+      mUniqueStrings.WriteElement(mWriter, JS::TrackedOutcomeString(outcome));
+    }
+    mWriter.EndArray();
   }
 };
 
 class StreamJSFramesOp : public JS::ForEachProfiledFrameOp
 {
-  JSRuntime* mRuntime;
   void* mReturnAddress;
-  UniqueJITOptimizations& mUniqueOpts;
-  JSStreamWriter& mWriter;
+  UniqueStacks::Stack& mStack;
+  unsigned mDepth;
 
 public:
-  StreamJSFramesOp(JSRuntime* aRuntime, void* aReturnAddr, UniqueJITOptimizations& aUniqueOpts,
-                   JSStreamWriter& aWriter)
-   : mRuntime(aRuntime)
-   , mReturnAddress(aReturnAddr)
-   , mUniqueOpts(aUniqueOpts)
-   , mWriter(aWriter)
+  StreamJSFramesOp(void* aReturnAddr, UniqueStacks::Stack& aStack)
+   : mReturnAddress(aReturnAddr)
+   , mStack(aStack)
+   , mDepth(0)
   { }
 
-  void operator()(const char* label, bool mightHaveTrackedOptimizations) override {
-    mWriter.BeginObject();
-      mWriter.NameValue("location", label);
-      JS::ProfilingFrameIterator::FrameKind frameKind =
-        JS::GetProfilingFrameKindFromNativeAddr(mRuntime, mReturnAddress);
-      MOZ_ASSERT(frameKind == JS::ProfilingFrameIterator::Frame_Ion ||
-                 frameKind == JS::ProfilingFrameIterator::Frame_Baseline);
-      const char* jitLevelString =
-        (frameKind == JS::ProfilingFrameIterator::Frame_Ion) ? "ion"
-                                                             : "baseline";
-      mWriter.NameValue("implementation", jitLevelString);
-      if (mightHaveTrackedOptimizations) {
-        Maybe<unsigned> optsIndex = mUniqueOpts.getIndex(mReturnAddress, mRuntime);
-        if (optsIndex.isSome()) {
-          mWriter.NameValue("optsIndex", optsIndex.value());
-        }
-      }
-    mWriter.EndObject();
+  unsigned depth() const {
+    MOZ_ASSERT(mDepth > 0);
+    return mDepth;
+  }
+
+  void operator()(const JS::ForEachProfiledFrameOp::FrameHandle& aFrameHandle) override {
+    UniqueStacks::OnStackFrameKey frameKey(mReturnAddress, mDepth, aFrameHandle);
+    mStack.AppendFrame(frameKey);
+    mDepth++;
   }
 };
 
-bool UniqueJITOptimizations::OptimizationKey::operator<(const OptimizationKey& other) const
+uint32_t UniqueJSONStrings::GetOrAddIndex(const char* aStr)
+{
+  uint32_t index;
+  if (mStringToIndexMap.Get(aStr, &index)) {
+    return index;
+  }
+  index = mStringToIndexMap.Count();
+  mStringToIndexMap.Put(aStr, index);
+  mStringTableWriter.StringElement(aStr);
+  return index;
+}
+
+bool UniqueStacks::FrameKey::operator==(const FrameKey& aOther) const
+{
+  return mLocation == aOther.mLocation &&
+         mLine == aOther.mLine &&
+         mCategory == aOther.mCategory &&
+         mJITAddress == aOther.mJITAddress &&
+         mJITDepth == aOther.mJITDepth;
+}
+
+bool UniqueStacks::StackKey::operator==(const StackKey& aOther) const
+{
+  MOZ_ASSERT_IF(mPrefix == aOther.mPrefix, mPrefixHash == aOther.mPrefixHash);
+  return mPrefix == aOther.mPrefix && mFrame == aOther.mFrame;
+}
+
+UniqueStacks::Stack::Stack(UniqueStacks& aUniqueStacks, const OnStackFrameKey& aRoot)
+ : mUniqueStacks(aUniqueStacks)
+ , mStack(aUniqueStacks.GetOrAddFrameIndex(aRoot))
+{
+}
+
+void UniqueStacks::Stack::AppendFrame(const OnStackFrameKey& aFrame)
 {
-  if (mEntryAddr == other.mEntryAddr) {
-    return mIndex < other.mIndex;
+  // Compute the prefix hash and index before mutating mStack.
+  uint32_t prefixHash = mStack.Hash();
+  uint32_t prefix = mUniqueStacks.GetOrAddStackIndex(mStack);
+  mStack.mPrefixHash = Some(prefixHash);
+  mStack.mPrefix = Some(prefix);
+  mStack.mFrame = mUniqueStacks.GetOrAddFrameIndex(aFrame);
+}
+
+uint32_t UniqueStacks::Stack::GetOrAddIndex() const
+{
+  return mUniqueStacks.GetOrAddStackIndex(mStack);
+}
+
+uint32_t UniqueStacks::FrameKey::Hash() const
+{
+  uint32_t hash = mozilla::HashString(mLocation.c_str(), mLocation.length());
+  if (mLine.isSome()) {
+    hash = mozilla::AddToHash(hash, *mLine);
   }
-  return mEntryAddr < other.mEntryAddr;
+  if (mCategory.isSome()) {
+    hash = mozilla::AddToHash(hash, *mCategory);
+  }
+  if (mJITAddress.isSome()) {
+    hash = mozilla::AddToHash(hash, *mJITAddress);
+    if (mJITDepth.isSome()) {
+      hash = mozilla::AddToHash(hash, *mJITDepth);
+    }
+  }
+  return hash;
+}
+
+uint32_t UniqueStacks::StackKey::Hash() const
+{
+  if (mPrefix.isNothing()) {
+    return mozilla::HashGeneric(mFrame);
+  }
+  return mozilla::AddToHash(*mPrefixHash, mFrame);
+}
+
+UniqueStacks::Stack UniqueStacks::BeginStack(const OnStackFrameKey& aRoot)
+{
+  return Stack(*this, aRoot);
 }
 
-Maybe<unsigned> UniqueJITOptimizations::getIndex(void* addr, JSRuntime* rt)
+UniqueStacks::UniqueStacks(JSRuntime* aRuntime)
+ : mRuntime(aRuntime)
+ , mFrameCount(0)
+{
+  mFrameTableWriter.StartBareList();
+  mStackTableWriter.StartBareList();
+}
+
+UniqueStacks::~UniqueStacks()
+{
+  mFrameTableWriter.EndBareList();
+  mStackTableWriter.EndBareList();
+}
+
+uint32_t UniqueStacks::GetOrAddStackIndex(const StackKey& aStack)
 {
-  void* entryAddr;
-  Maybe<uint8_t> optIndex = JS::TrackedOptimizationIndexAtAddr(rt, addr, &entryAddr);
-  if (optIndex.isNothing()) {
-    return Nothing();
+  uint32_t index;
+  if (mStackToIndexMap.Get(aStack, &index)) {
+    MOZ_ASSERT(index < mStackToIndexMap.Count());
+    return index;
+  }
+
+  index = mStackToIndexMap.Count();
+  mStackToIndexMap.Put(aStack, index);
+  StreamStack(aStack);
+  return index;
+}
+
+uint32_t UniqueStacks::GetOrAddFrameIndex(const OnStackFrameKey& aFrame)
+{
+  uint32_t index;
+  if (mFrameToIndexMap.Get(aFrame, &index)) {
+    MOZ_ASSERT(index < mFrameCount);
+    return index;
   }
 
-  OptimizationKey key;
-  key.mEntryAddr = entryAddr;
-  key.mIndex = optIndex.value();
-
-  auto iter = mOptToIndexMap.find(key);
-  if (iter != mOptToIndexMap.end()) {
-    MOZ_ASSERT(iter->second < mOpts.length());
-    return Some(iter->second);
+  // If aFrame isn't canonical, forward it to the canonical frame's index.
+  if (aFrame.mJITFrameHandle) {
+    void* canonicalAddr = aFrame.mJITFrameHandle->canonicalAddress();
+    if (canonicalAddr != *aFrame.mJITAddress) {
+      OnStackFrameKey canonicalKey(canonicalAddr, *aFrame.mJITDepth, *aFrame.mJITFrameHandle);
+      uint32_t canonicalIndex = GetOrAddFrameIndex(canonicalKey);
+      mFrameToIndexMap.Put(aFrame, canonicalIndex);
+      return canonicalIndex;
+    }
   }
 
-  unsigned keyIndex = mOpts.length();
-  mOptToIndexMap.insert(std::make_pair(key, keyIndex));
-  MOZ_ALWAYS_TRUE(mOpts.append(key));
-  return Some(keyIndex);
+  // A manual count is used instead of mFrameToIndexMap.Count() due to
+  // forwarding of canonical JIT frames above.
+  index = mFrameCount++;
+  mFrameToIndexMap.Put(aFrame, index);
+  StreamFrame(aFrame);
+  return index;
+}
+
+uint32_t UniqueStacks::LookupJITFrameDepth(void* aAddr)
+{
+  uint32_t depth;
+  if (mJITFrameDepthMap.Get(aAddr, &depth)) {
+    MOZ_ASSERT(depth > 0);
+    return depth;
+  }
+  return 0;
+}
+
+void UniqueStacks::AddJITFrameDepth(void* aAddr, unsigned depth)
+{
+  mJITFrameDepthMap.Put(aAddr, depth);
+}
+
+void UniqueStacks::SpliceFrameTableElements(SpliceableJSONWriter& aWriter) const
+{
+  aWriter.Splice(mFrameTableWriter.WriteFunc());
+}
+
+void UniqueStacks::SpliceStackTableElements(SpliceableJSONWriter& aWriter) const
+{
+  aWriter.Splice(mStackTableWriter.WriteFunc());
 }
 
-void UniqueJITOptimizations::stream(JSStreamWriter& b, JSRuntime* rt)
+void UniqueStacks::StreamStack(const StackKey& aStack)
 {
-  for (size_t i = 0; i < mOpts.length(); i++) {
-    b.BeginObject();
-    b.Name("types");
-    b.BeginArray();
-    StreamOptimizationTypeInfoOp typeInfoOp(b);
-    JS::ForEachTrackedOptimizationTypeInfo(rt, mOpts[i].mEntryAddr, mOpts[i].mIndex,
-                                           typeInfoOp);
-    b.EndArray();
+  // Schema:
+  //   [prefix, frame]
 
-    b.Name("attempts");
-    b.BeginArray();
-    JSScript* script;
-    jsbytecode* pc;
-    StreamOptimizationAttemptsOp attemptOp(b);
-    JS::ForEachTrackedOptimizationAttempt(rt, mOpts[i].mEntryAddr, mOpts[i].mIndex,
-                                          attemptOp, &script, &pc);
-    b.EndArray();
-
-    if (JSAtom* name = js::GetPropertyNameFromPC(script, pc)) {
-      char buf[512];
-      JS_PutEscapedFlatString(buf, mozilla::ArrayLength(buf), js::AtomToFlatString(name), 0);
-      b.NameValue("propertyName", buf);
+  mStackTableWriter.StartArrayElement();
+  {
+    if (aStack.mPrefix.isSome()) {
+      mStackTableWriter.IntElement(*aStack.mPrefix);
+    } else {
+      mStackTableWriter.NullElement();
     }
-
-    unsigned line, column;
-    line = JS_PCToLineNumber(script, pc, &column);
-    b.NameValue("line", line);
-    b.NameValue("column", column);
-    b.EndObject();
+    mStackTableWriter.IntElement(aStack.mFrame);
   }
+  mStackTableWriter.EndArray();
 }
 
-void ProfileBuffer::StreamSamplesToJSObject(JSStreamWriter& b, int aThreadId,
-                                            float aSinceTime, JSRuntime* rt,
-                                            UniqueJITOptimizations& aUniqueOpts)
+void UniqueStacks::StreamFrame(const OnStackFrameKey& aFrame)
 {
-  bool sample = false;
+  // Schema:
+  //   [location, implementation, optimizations, line, category]
+
+  mFrameTableWriter.StartArrayElement();
+  if (!aFrame.mJITFrameHandle) {
+    mUniqueStrings.WriteElement(mFrameTableWriter, aFrame.mLocation.c_str());
+    if (aFrame.mLine.isSome()) {
+      mFrameTableWriter.NullElement(); // implementation
+      mFrameTableWriter.NullElement(); // optimizations
+      mFrameTableWriter.IntElement(*aFrame.mLine);
+    }
+    if (aFrame.mCategory.isSome()) {
+      if (aFrame.mLine.isNothing()) {
+        mFrameTableWriter.NullElement(); // line
+      }
+      mFrameTableWriter.IntElement(*aFrame.mCategory);
+    }
+  } else {
+    const JS::ForEachProfiledFrameOp::FrameHandle& jitFrame = *aFrame.mJITFrameHandle;
+
+    mUniqueStrings.WriteElement(mFrameTableWriter, jitFrame.label());
+
+    JS::ProfilingFrameIterator::FrameKind frameKind = jitFrame.frameKind();
+    MOZ_ASSERT(frameKind == JS::ProfilingFrameIterator::Frame_Ion ||
+               frameKind == JS::ProfilingFrameIterator::Frame_Baseline);
+    mUniqueStrings.WriteElement(mFrameTableWriter,
+                                frameKind == JS::ProfilingFrameIterator::Frame_Ion
+                                ? "ion"
+                                : "baseline");
+
+    if (jitFrame.hasTrackedOptimizations()) {
+      mFrameTableWriter.StartObjectElement();
+      {
+        mFrameTableWriter.StartArrayProperty("types");
+        {
+          StreamOptimizationTypeInfoOp typeInfoOp(mFrameTableWriter, mUniqueStrings);
+          jitFrame.forEachOptimizationTypeInfo(typeInfoOp);
+        }
+        mFrameTableWriter.EndArray();
+
+        JS::Rooted<JSScript*> script(mRuntime);
+        jsbytecode* pc;
+        mFrameTableWriter.StartObjectProperty("attempts");
+        {
+          {
+            JSONSchemaWriter schema(mFrameTableWriter);
+            schema.WriteField("strategy");
+            schema.WriteField("outcome");
+          }
+
+          mFrameTableWriter.StartArrayProperty("data");
+          {
+            StreamOptimizationAttemptsOp attemptOp(mFrameTableWriter, mUniqueStrings);
+            jitFrame.forEachOptimizationAttempt(attemptOp, script.address(), &pc);
+          }
+          mFrameTableWriter.EndArray();
+        }
+        mFrameTableWriter.EndObject();
+
+        if (JSAtom* name = js::GetPropertyNameFromPC(script, pc)) {
+          char buf[512];
+          JS_PutEscapedFlatString(buf, mozilla::ArrayLength(buf), js::AtomToFlatString(name), 0);
+          mUniqueStrings.WriteProperty(mFrameTableWriter, "propertyName", buf);
+        }
+
+        unsigned line, column;
+        line = JS_PCToLineNumber(script, pc, &column);
+        mFrameTableWriter.IntProperty("line", line);
+        mFrameTableWriter.IntProperty("column", column);
+      }
+      mFrameTableWriter.EndObject();
+    }
+  }
+  mFrameTableWriter.EndArray();
+}
+
+struct ProfileSample
+{
+  uint32_t mStack;
+  Maybe<float> mTime;
+  Maybe<float> mResponsiveness;
+  Maybe<float> mRSS;
+  Maybe<float> mUSS;
+  Maybe<int> mFrameNumber;
+  Maybe<float> mPower;
+};
+
+static void WriteSample(SpliceableJSONWriter& aWriter, ProfileSample& aSample)
+{
+  // Schema:
+  //   [stack, time, responsiveness, rss, uss, frameNumber, power]
+
+  aWriter.StartArrayElement();
+  {
+    // The last non-null index is tracked to save space in the JSON by avoid
+    // emitting 'null's at the end of the array, as they're only needed if
+    // followed by non-null elements.
+    uint32_t index = 0;
+    uint32_t lastNonNullIndex = 0;
+
+    aWriter.IntElement(aSample.mStack);
+    index++;
+
+    if (aSample.mTime.isSome()) {
+      lastNonNullIndex = index;
+      aWriter.DoubleElement(*aSample.mTime);
+    }
+    index++;
+
+    if (aSample.mResponsiveness.isSome()) {
+      aWriter.NullElements(index - lastNonNullIndex - 1);
+      lastNonNullIndex = index;
+      aWriter.DoubleElement(*aSample.mResponsiveness);
+    }
+    index++;
+
+    if (aSample.mRSS.isSome()) {
+      aWriter.NullElements(index - lastNonNullIndex - 1);
+      lastNonNullIndex = index;
+      aWriter.DoubleElement(*aSample.mRSS);
+    }
+    index++;
+
+    if (aSample.mUSS.isSome()) {
+      aWriter.NullElements(index - lastNonNullIndex - 1);
+      lastNonNullIndex = index;
+      aWriter.DoubleElement(*aSample.mUSS);
+    }
+    index++;
+
+    if (aSample.mFrameNumber.isSome()) {
+      aWriter.NullElements(index - lastNonNullIndex - 1);
+      lastNonNullIndex = index;
+      aWriter.IntElement(*aSample.mFrameNumber);
+    }
+    index++;
+
+    if (aSample.mPower.isSome()) {
+      aWriter.NullElements(index - lastNonNullIndex - 1);
+      lastNonNullIndex = index;
+      aWriter.DoubleElement(*aSample.mPower);
+    }
+    index++;
+  }
+  aWriter.EndArray();
+}
+
+void ProfileBuffer::StreamSamplesToJSON(SpliceableJSONWriter& aWriter, int aThreadId,
+                                        float aSinceTime, JSRuntime* aRuntime,
+                                        UniqueStacks& aUniqueStacks)
+{
+  Maybe<ProfileSample> sample;
   int readPos = mReadPos;
   int currentThreadID = -1;
-  float currentTime = 0;
-  bool hasCurrentTime = false;
+  Maybe<float> currentTime;
+
   while (readPos != mWritePos) {
     ProfileEntry entry = mEntries[readPos];
     if (entry.mTagName == 'T') {
       currentThreadID = entry.mTagInt;
-      hasCurrentTime = false;
+      currentTime.reset();
       int readAheadPos = (readPos + 1) % mEntrySize;
       if (readAheadPos != mWritePos) {
         ProfileEntry readAheadEntry = mEntries[readAheadPos];
         if (readAheadEntry.mTagName == 't') {
-          currentTime = readAheadEntry.mTagFloat;
-          hasCurrentTime = true;
+          currentTime = Some(readAheadEntry.mTagFloat);
         }
       }
     }
-    if (currentThreadID == aThreadId && (!hasCurrentTime || currentTime >= aSinceTime)) {
+    if (currentThreadID == aThreadId && (currentTime.isNothing() || *currentTime >= aSinceTime)) {
       switch (entry.mTagName) {
-        case 'r':
-          {
-            if (sample) {
-              b.NameValue("responsiveness", entry.mTagFloat);
-            }
-          }
-          break;
-        case 'p':
-          {
-            if (sample) {
-              b.NameValue("power", entry.mTagFloat);
-            }
-          }
-          break;
-        case 'R':
-          {
-            if (sample) {
-              b.NameValue("rss", entry.mTagFloat);
-            }
-          }
-          break;
-        case 'U':
-          {
-            if (sample) {
-              b.NameValue("uss", entry.mTagFloat);
-            }
+      case 'r':
+        if (sample.isSome()) {
+          sample->mResponsiveness = Some(entry.mTagFloat);
+        }
+        break;
+      case 'p':
+        if (sample.isSome()) {
+          sample->mPower = Some(entry.mTagFloat);
+        }
+        break;
+      case 'R':
+        if (sample.isSome()) {
+          sample->mRSS = Some(entry.mTagFloat);
+        }
+        break;
+      case 'U':
+        if (sample.isSome()) {
+          sample->mUSS = Some(entry.mTagFloat);
+         }
+        break;
+      case 'f':
+        if (sample.isSome()) {
+          sample->mFrameNumber = Some(entry.mTagInt);
+        }
+        break;
+      case 's':
+        {
+          // end the previous sample if there was one
+          if (sample.isSome()) {
+            WriteSample(aWriter, *sample);
+            sample.reset();
           }
-          break;
-        case 'f':
-          {
-            if (sample) {
-              b.NameValue("frameNumber", entry.mTagInt);
-            }
-          }
-          break;
-        case 't':
-          {
-            // FIXMEshu: this case is only needed because filtering by
-            // aSinceTime is broken if the unwinder thread is used, due to
-            // its placement of 't' tags.
-            //
-            // UnwinderTick is slated for removal in bug 1141712. Remove
-            // this case once it lands.
-            if (sample && (currentTime != entry.mTagFloat)) {
-              b.NameValue("time", entry.mTagFloat);
-            }
-          }
-          break;
-        case 's':
-          {
-            // end the previous sample if there was one
-            if (sample) {
-              b.EndObject();
-            }
-            // begin the next sample
-            b.BeginObject();
+          // begin the next sample
+          sample.emplace();
+          sample->mTime = currentTime;
+
+          // Seek forward through the entire sample, looking for frames
+          // this is an easier approach to reason about than adding more
+          // control variables and cases to the loop that goes through the buffer once
+
+          UniqueStacks::Stack stack =
+            aUniqueStacks.BeginStack(UniqueStacks::OnStackFrameKey("(root)"));
 
-            sample = true;
+          int framePos = (readPos + 1) % mEntrySize;
+          ProfileEntry frame = mEntries[framePos];
+          while (framePos != mWritePos && frame.mTagName != 's' && frame.mTagName != 'T') {
+            int incBy = 1;
+            frame = mEntries[framePos];
 
-            if (hasCurrentTime) {
-              b.NameValue("time", currentTime);
+            // Read ahead to the next tag, if it's a 'd' tag process it now
+            const char* tagStringData = frame.mTagData;
+            int readAheadPos = (framePos + 1) % mEntrySize;
+            char tagBuff[DYNAMIC_MAX_STRING];
+            // Make sure the string is always null terminated if it fills up
+            // DYNAMIC_MAX_STRING-2
+            tagBuff[DYNAMIC_MAX_STRING-1] = '\0';
+
+            if (readAheadPos != mWritePos && mEntries[readAheadPos].mTagName == 'd') {
+              tagStringData = processDynamicTag(framePos, &incBy, tagBuff);
             }
 
-            // Seek forward through the entire sample, looking for frames
-            // this is an easier approach to reason about than adding more
-            // control variables and cases to the loop that goes through the buffer once
-            b.Name("frames");
-            b.BeginArray();
-
-              b.BeginObject();
-                b.NameValue("location", "(root)");
-              b.EndObject();
-
-              int framePos = (readPos + 1) % mEntrySize;
-              ProfileEntry frame = mEntries[framePos];
-              while (framePos != mWritePos && frame.mTagName != 's' && frame.mTagName != 'T') {
-                int incBy = 1;
-                frame = mEntries[framePos];
-
-                // Read ahead to the next tag, if it's a 'd' tag process it now
-                const char* tagStringData = frame.mTagData;
-                int readAheadPos = (framePos + 1) % mEntrySize;
-                char tagBuff[DYNAMIC_MAX_STRING];
-                // Make sure the string is always null terminated if it fills up
-                // DYNAMIC_MAX_STRING-2
-                tagBuff[DYNAMIC_MAX_STRING-1] = '\0';
-
-                if (readAheadPos != mWritePos && mEntries[readAheadPos].mTagName == 'd') {
-                  tagStringData = processDynamicTag(framePos, &incBy, tagBuff);
+            // Write one frame. It can have either
+            // 1. only location - 'l' containing a memory address
+            // 2. location and line number - 'c' followed by 'd's,
+            // an optional 'n' and an optional 'y'
+            // 3. a JIT return address - 'j' containing native code address
+            if (frame.mTagName == 'l') {
+              // Bug 753041
+              // We need a double cast here to tell GCC that we don't want to sign
+              // extend 32-bit addresses starting with 0xFXXXXXX.
+              unsigned long long pc = (unsigned long long)(uintptr_t)frame.mTagPtr;
+              snprintf(tagBuff, DYNAMIC_MAX_STRING, "%#llx", pc);
+              stack.AppendFrame(UniqueStacks::OnStackFrameKey(tagBuff));
+            } else if (frame.mTagName == 'c') {
+              UniqueStacks::OnStackFrameKey frameKey(tagStringData);
+              readAheadPos = (framePos + incBy) % mEntrySize;
+              if (readAheadPos != mWritePos &&
+                  mEntries[readAheadPos].mTagName == 'n') {
+                frameKey.mLine = Some((unsigned) mEntries[readAheadPos].mTagInt);
+                incBy++;
+              }
+              readAheadPos = (framePos + incBy) % mEntrySize;
+              if (readAheadPos != mWritePos &&
+                  mEntries[readAheadPos].mTagName == 'y') {
+                frameKey.mCategory = Some((unsigned) mEntries[readAheadPos].mTagInt);
+                incBy++;
+              }
+              stack.AppendFrame(frameKey);
+            } else if (frame.mTagName == 'J') {
+              // A JIT frame may expand to multiple frames due to inlining.
+              void* pc = frame.mTagPtr;
+              unsigned depth = aUniqueStacks.LookupJITFrameDepth(pc);
+              if (depth == 0) {
+                StreamJSFramesOp framesOp(pc, stack);
+                JS::ForEachProfiledFrame(aRuntime, pc, framesOp);
+                aUniqueStacks.AddJITFrameDepth(pc, framesOp.depth());
+              } else {
+                for (unsigned i = 0; i < depth; i++) {
+                  UniqueStacks::OnStackFrameKey inlineFrameKey(pc, i);
+                  stack.AppendFrame(inlineFrameKey);
                 }
+              }
+            }
+            framePos = (framePos + incBy) % mEntrySize;
+          }
 
-                // Write one frame. It can have either
-                // 1. only location - 'l' containing a memory address
-                // 2. location and line number - 'c' followed by 'd's,
-                // an optional 'n' and an optional 'y'
-                if (frame.mTagName == 'l') {
-                  b.BeginObject();
-                    // Bug 753041
-                    // We need a double cast here to tell GCC that we don't want to sign
-                    // extend 32-bit addresses starting with 0xFXXXXXX.
-                    unsigned long long pc = (unsigned long long)(uintptr_t)frame.mTagPtr;
-                    snprintf(tagBuff, DYNAMIC_MAX_STRING, "%#llx", pc);
-                    b.NameValue("location", tagBuff);
-                  b.EndObject();
-                } else if (frame.mTagName == 'c') {
-                  b.BeginObject();
-                    b.NameValue("location", tagStringData);
-                    readAheadPos = (framePos + incBy) % mEntrySize;
-                    if (readAheadPos != mWritePos &&
-                        mEntries[readAheadPos].mTagName == 'n') {
-                      b.NameValue("line", mEntries[readAheadPos].mTagInt);
-                      incBy++;
-                    }
-                    readAheadPos = (framePos + incBy) % mEntrySize;
-                    if (readAheadPos != mWritePos &&
-                        mEntries[readAheadPos].mTagName == 'y') {
-                      b.NameValue("category", mEntries[readAheadPos].mTagInt);
-                      incBy++;
-                    }
-                  b.EndObject();
-                } else if (frame.mTagName == 'J') {
-                  void* pc = frame.mTagPtr;
-                  StreamJSFramesOp framesOp(rt, pc, aUniqueOpts, b);
-                  JS::ForEachProfiledFrame(rt, pc, framesOp);
-                }
-                framePos = (framePos + incBy) % mEntrySize;
-              }
-            b.EndArray();
-          }
+          sample->mStack = stack.GetOrAddIndex();
           break;
+        }
       }
     }
     readPos = (readPos + 1) % mEntrySize;
   }
-  if (sample) {
-    b.EndObject();
+  if (sample.isSome()) {
+    WriteSample(aWriter, *sample);
   }
 }
 
-void ProfileBuffer::StreamMarkersToJSObject(JSStreamWriter& b, int aThreadId, float aSinceTime)
+void ProfileBuffer::StreamMarkersToJSON(SpliceableJSONWriter& aWriter, int aThreadId,
+                                        float aSinceTime, UniqueStacks& aUniqueStacks)
 {
   int readPos = mReadPos;
   int currentThreadID = -1;
   while (readPos != mWritePos) {
     ProfileEntry entry = mEntries[readPos];
     if (entry.mTagName == 'T') {
       currentThreadID = entry.mTagInt;
     } else if (currentThreadID == aThreadId && entry.mTagName == 'm') {
       const ProfilerMarker* marker = entry.getMarker();
       if (marker->GetTime() >= aSinceTime) {
-        marker->StreamJSObject(b);
+        entry.getMarker()->StreamJSON(aWriter, aUniqueStacks);
       }
     }
     readPos = (readPos + 1) % mEntrySize;
   }
 }
 
 int ProfileBuffer::FindLastSampleOfThread(int aThreadId)
 {
@@ -672,137 +931,197 @@ void ThreadProfile::addStoredMarker(Prof
 
 void ThreadProfile::IterateTags(IterateTagsCallback aCallback)
 {
   mBuffer->IterateTagsForThread(aCallback, mThreadId);
 }
 
 void ThreadProfile::ToStreamAsJSON(std::ostream& stream, float aSinceTime)
 {
-  JSStreamWriter b(stream);
-  StreamJSObject(b, aSinceTime);
+  SpliceableJSONWriter b(MakeUnique<OStreamJSONWriteFunc>(stream));
+  StreamJSON(b, aSinceTime);
 }
 
-void ThreadProfile::StreamJSObject(JSStreamWriter& b, float aSinceTime)
+void ThreadProfile::StreamJSON(SpliceableJSONWriter& aWriter, float aSinceTime)
 {
-  b.BeginObject();
-    // Thread meta data
-    if (XRE_GetProcessType() == GeckoProcessType_Plugin) {
-      // TODO Add the proper plugin name
-      b.NameValue("name", "Plugin");
-    } else if (XRE_GetProcessType() == GeckoProcessType_Content) {
-      // This isn't going to really help once we have multiple content
-      // processes, but it'll do for now.
-      b.NameValue("name", "Content");
-    } else {
-      b.NameValue("name", Name());
+  // mUniqueStacks may already be emplaced from FlushSamplesAndMarkers.
+  if (!mUniqueStacks.isSome()) {
+    mUniqueStacks.emplace(mPseudoStack->mRuntime);
+  }
+
+  aWriter.Start(SpliceableJSONWriter::SingleLineStyle);
+  {
+    StreamSamplesAndMarkers(aWriter, aSinceTime, *mUniqueStacks);
+
+    aWriter.StartObjectProperty("stackTable");
+    {
+      {
+        JSONSchemaWriter schema(aWriter);
+        schema.WriteField("prefix");
+        schema.WriteField("frame");
+      }
+
+      aWriter.StartArrayProperty("data");
+      {
+        mUniqueStacks->SpliceStackTableElements(aWriter);
+      }
+      aWriter.EndArray();
+    }
+    aWriter.EndObject();
+
+    aWriter.StartObjectProperty("frameTable");
+    {
+      {
+        JSONSchemaWriter schema(aWriter);
+        schema.WriteField("location");
+        schema.WriteField("implementation");
+        schema.WriteField("optimizations");
+        schema.WriteField("line");
+        schema.WriteField("category");
+      }
+
+      aWriter.StartArrayProperty("data");
+      {
+        mUniqueStacks->SpliceFrameTableElements(aWriter);
+      }
+      aWriter.EndArray();
     }
-    b.NameValue("tid", static_cast<int>(mThreadId));
+    aWriter.EndObject();
 
-    UniqueJITOptimizations uniqueOpts;
+    aWriter.StartArrayProperty("stringTable");
+    {
+      mUniqueStacks->mUniqueStrings.SpliceStringTableElements(aWriter);
+    }
+    aWriter.EndArray();
+  }
+  aWriter.End();
+
+  mUniqueStacks.reset();
+}
 
-    b.Name("samples");
-    b.BeginArray();
-      if (!mSavedStreamedSamples.empty()) {
+void ThreadProfile::StreamSamplesAndMarkers(SpliceableJSONWriter& aWriter, float aSinceTime,
+                                            UniqueStacks& aUniqueStacks)
+{
+  // Thread meta data
+  if (XRE_GetProcessType() == GeckoProcessType_Plugin) {
+    // TODO Add the proper plugin name
+    aWriter.StringProperty("name", "Plugin");
+  } else if (XRE_GetProcessType() == GeckoProcessType_Content) {
+    // This isn't going to really help once we have multiple content
+    // processes, but it'll do for now.
+    aWriter.StringProperty("name", "Content");
+  } else {
+    aWriter.StringProperty("name", Name());
+  }
+  aWriter.IntProperty("tid", static_cast<int>(mThreadId));
+
+  aWriter.StartObjectProperty("samples");
+  {
+    {
+      JSONSchemaWriter schema(aWriter);
+      schema.WriteField("stack");
+      schema.WriteField("time");
+      schema.WriteField("responsiveness");
+      schema.WriteField("rss");
+      schema.WriteField("uss");
+      schema.WriteField("frameNumber");
+      schema.WriteField("power");
+    }
+
+    aWriter.StartArrayProperty("data");
+    {
+      if (mSavedStreamedSamples) {
         // We would only have saved streamed samples during shutdown
         // streaming, which cares about dumping the entire buffer, and thus
         // should have passed in 0 for aSinceTime.
         MOZ_ASSERT(aSinceTime == 0);
-        b.SpliceArrayElements(mSavedStreamedSamples.c_str());
-        mSavedStreamedSamples.clear();
+        aWriter.Splice(mSavedStreamedSamples.get());
+        mSavedStreamedSamples.reset();
       }
-      mBuffer->StreamSamplesToJSObject(b, mThreadId, aSinceTime, mPseudoStack->mRuntime,
-                                       uniqueOpts);
-    b.EndArray();
+      mBuffer->StreamSamplesToJSON(aWriter, mThreadId, aSinceTime,
+                                   mPseudoStack->mRuntime, aUniqueStacks);
+    }
+    aWriter.EndArray();
+  }
+  aWriter.EndObject();
 
-    // Having saved streamed optimizations implies the JS engine has
-    // shutdown. If the JS engine is gone, we shouldn't have any new JS
-    // samples, and thus no optimizations.
-    if (!mSavedStreamedOptimizations.empty()) {
-      MOZ_ASSERT(aSinceTime == 0);
-      MOZ_ASSERT(uniqueOpts.empty());
-      b.Name("optimizations");
-      b.BeginArray();
-        b.SpliceArrayElements(mSavedStreamedOptimizations.c_str());
-        mSavedStreamedOptimizations.clear();
-      b.EndArray();
-    } else if (!uniqueOpts.empty()) {
-      b.Name("optimizations");
-      b.BeginArray();
-        uniqueOpts.stream(b, mPseudoStack->mRuntime);
-      b.EndArray();
+  aWriter.StartObjectProperty("markers");
+  {
+    {
+      JSONSchemaWriter schema(aWriter);
+      schema.WriteField("name");
+      schema.WriteField("time");
+      schema.WriteField("data");
     }
 
-    b.Name("markers");
-    b.BeginArray();
-      if (!mSavedStreamedMarkers.empty()) {
+    aWriter.StartArrayProperty("data");
+    {
+      if (mSavedStreamedMarkers) {
         MOZ_ASSERT(aSinceTime == 0);
-        b.SpliceArrayElements(mSavedStreamedMarkers.c_str());
-        mSavedStreamedMarkers.clear();
+        aWriter.Splice(mSavedStreamedMarkers.get());
+        mSavedStreamedMarkers.reset();
       }
-      mBuffer->StreamMarkersToJSObject(b, mThreadId, aSinceTime);
-    b.EndArray();
-  b.EndObject();
+      mBuffer->StreamMarkersToJSON(aWriter, mThreadId, aSinceTime, aUniqueStacks);
+    }
+    aWriter.EndArray();
+  }
+  aWriter.EndObject();
 }
 
 void ThreadProfile::FlushSamplesAndMarkers()
 {
   // This function is used to serialize the current buffer just before
   // JSRuntime destruction.
   MOZ_ASSERT(mPseudoStack->mRuntime);
 
   // Unlike StreamJSObject, do not surround the samples in brackets by calling
-  // b.{Begin,End}Array. The result string will be a comma-separated list of
-  // JSON object literals that will prepended by StreamJSObject into an
-  // existing array.
-  std::stringstream ss;
-  JSStreamWriter b(ss);
-  UniqueJITOptimizations uniqueOpts;
-  b.BeginBareList();
-  mBuffer->StreamSamplesToJSObject(b, mThreadId, 0, mPseudoStack->mRuntime, uniqueOpts);
-  b.EndBareList();
-  mSavedStreamedSamples = ss.str();
+  // aWriter.{Start,End}BareList. The result string will be a comma-separated
+  // list of JSON object literals that will prepended by StreamJSObject into
+  // an existing array.
+  //
+  // Note that the UniqueStacks instance is persisted so that the frame-index
+  // mapping is stable across JS shutdown.
+  mUniqueStacks.emplace(mPseudoStack->mRuntime);
 
-  // Reuse the stringstream.
-  ss.str("");
-  ss.clear();
-
-  if (!uniqueOpts.empty()) {
-    b.BeginBareList();
-      uniqueOpts.stream(b, mPseudoStack->mRuntime);
+  {
+    SpliceableChunkedJSONWriter b;
+    b.StartBareList();
+    {
+      mBuffer->StreamSamplesToJSON(b, mThreadId, /* aSinceTime = */ 0,
+                                   mPseudoStack->mRuntime, *mUniqueStacks);
+    }
     b.EndBareList();
-    mSavedStreamedOptimizations = ss.str();
+    mSavedStreamedSamples = b.WriteFunc()->CopyData();
   }
 
-  // Reuse the stringstream.
-  ss.str("");
-  ss.clear();
-
-  b.BeginBareList();
-    mBuffer->StreamMarkersToJSObject(b, mThreadId, 0);
-  b.EndBareList();
-  mSavedStreamedMarkers = ss.str();
+  {
+    SpliceableChunkedJSONWriter b;
+    b.StartBareList();
+    {
+      mBuffer->StreamMarkersToJSON(b, mThreadId, /* aSinceTime = */ 0, *mUniqueStacks);
+    }
+    b.EndBareList();
+    mSavedStreamedMarkers = b.WriteFunc()->CopyData();
+  }
 
   // Reset the buffer. Attempting to symbolicate JS samples after mRuntime has
   // gone away will crash.
   mBuffer->reset();
 }
 
-JSObject* ThreadProfile::ToJSObject(JSContext *aCx, float aSinceTime)
+JSObject* ThreadProfile::ToJSObject(JSContext* aCx, float aSinceTime)
 {
   JS::RootedValue val(aCx);
-  std::stringstream ss;
   {
-    // Define a scope to prevent a moving GC during ~JSStreamWriter from
-    // trashing the return value.
-    JSStreamWriter b(ss);
-    StreamJSObject(b, aSinceTime);
-    NS_ConvertUTF8toUTF16 js_string(nsDependentCString(ss.str().c_str()));
-    JS_ParseJSON(aCx, static_cast<const char16_t*>(js_string.get()),
-                 js_string.Length(), &val);
+    SpliceableChunkedJSONWriter b;
+    StreamJSON(b, aSinceTime);
+    UniquePtr<char[]> buf = b.WriteFunc()->CopyData();
+    NS_ConvertUTF8toUTF16 js_string(nsDependentCString(buf.get()));
+    MOZ_ALWAYS_TRUE(JS_ParseJSON(aCx, static_cast<const char16_t*>(js_string.get()),
+                                 js_string.Length(), &val));
   }
   return &val.toObject();
 }
 
 PseudoStack* ThreadProfile::GetPseudoStack()
 {
   return mPseudoStack;
 }
--- a/tools/profiler/ProfileEntry.h
+++ b/tools/profiler/ProfileEntry.h
@@ -2,23 +2,26 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef MOZ_PROFILE_ENTRY_H
 #define MOZ_PROFILE_ENTRY_H
 
-#include <map>
 #include <ostream>
 #include "GeckoProfiler.h"
 #include "platform.h"
-#include "JSStreamWriter.h"
+#include "ProfileJSONWriter.h"
 #include "ProfilerBacktrace.h"
 #include "nsRefPtr.h"
+#include "nsHashKeys.h"
+#include "nsDataHashtable.h"
+#include "js/ProfilingFrameIterator.h"
+#include "js/TrackedOptimizationInfo.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/Mutex.h"
 #include "mozilla/Vector.h"
 #include "gtest/MozGtestFriend.h"
 #include "mozilla/UniquePtr.h"
 
 class ThreadProfile;
 
@@ -69,47 +72,162 @@ private:
   };
   char mTagName;
 };
 
 #pragma pack(pop)
 
 typedef void (*IterateTagsCallback)(const ProfileEntry& entry, const char* tagStringData);
 
-class UniqueJITOptimizations {
- public:
-  bool empty() const {
-    return mOpts.empty();
+class UniqueJSONStrings
+{
+public:
+  UniqueJSONStrings() {
+    mStringTableWriter.StartBareList();
+  }
+
+  ~UniqueJSONStrings() {
+    mStringTableWriter.EndBareList();
+  }
+
+  void SpliceStringTableElements(SpliceableJSONWriter& aWriter) const {
+    aWriter.Splice(mStringTableWriter.WriteFunc());
+  }
+
+  void WriteProperty(mozilla::JSONWriter& aWriter, const char* aName, const char* aStr) {
+    aWriter.IntProperty(aName, GetOrAddIndex(aStr));
+  }
+
+  void WriteElement(mozilla::JSONWriter& aWriter, const char* aStr) {
+    aWriter.IntElement(GetOrAddIndex(aStr));
   }
 
-  mozilla::Maybe<unsigned> getIndex(void* addr, JSRuntime* rt);
-  void stream(JSStreamWriter& b, JSRuntime* rt);
+  uint32_t GetOrAddIndex(const char* aStr);
+
+private:
+  SpliceableChunkedJSONWriter mStringTableWriter;
+  nsDataHashtable<nsCharPtrHashKey, uint32_t> mStringToIndexMap;
+};
+
+class UniqueStacks
+{
+public:
+  struct FrameKey {
+    std::string mLocation;
+    mozilla::Maybe<unsigned> mLine;
+    mozilla::Maybe<unsigned> mCategory;
+    mozilla::Maybe<void*> mJITAddress;
+    mozilla::Maybe<uint32_t> mJITDepth;
+
+    explicit FrameKey(const char* aLocation)
+     : mLocation(aLocation)
+    { }
 
- private:
-  struct OptimizationKey {
-    void* mEntryAddr;
-    uint8_t mIndex;
-    bool operator<(const OptimizationKey& other) const;
+    FrameKey(void* aJITAddress, uint32_t aJITDepth)
+     : mJITAddress(mozilla::Some(aJITAddress))
+     , mJITDepth(mozilla::Some(aJITDepth))
+    { }
+
+    uint32_t Hash() const;
+    bool operator==(const FrameKey& aOther) const;
+  };
+
+  // A FrameKey that holds a scoped reference to a JIT FrameHandle.
+  struct MOZ_STACK_CLASS OnStackFrameKey : public FrameKey {
+    const JS::ForEachProfiledFrameOp::FrameHandle* mJITFrameHandle;
+
+    explicit OnStackFrameKey(const char* aLocation)
+      : FrameKey(aLocation)
+      , mJITFrameHandle(nullptr)
+    { }
+
+    OnStackFrameKey(void* aJITAddress, unsigned aJITDepth)
+      : FrameKey(aJITAddress, aJITDepth)
+      , mJITFrameHandle(nullptr)
+    { }
+
+    OnStackFrameKey(void* aJITAddress, unsigned aJITDepth,
+                    const JS::ForEachProfiledFrameOp::FrameHandle& aJITFrameHandle)
+      : FrameKey(aJITAddress, aJITDepth)
+      , mJITFrameHandle(&aJITFrameHandle)
+    { }
   };
 
-  mozilla::Vector<OptimizationKey> mOpts;
-  std::map<OptimizationKey, unsigned> mOptToIndexMap;
+  struct StackKey {
+    mozilla::Maybe<uint32_t> mPrefixHash;
+    mozilla::Maybe<uint32_t> mPrefix;
+    uint32_t mFrame;
+
+    explicit StackKey(uint32_t aFrame)
+     : mFrame(aFrame)
+    { }
+
+    uint32_t Hash() const;
+    bool operator==(const StackKey& aOther) const;
+  };
+
+  class Stack {
+  public:
+    Stack(UniqueStacks& aUniqueStacks, const OnStackFrameKey& aRoot);
+
+    void AppendFrame(const OnStackFrameKey& aFrame);
+    uint32_t GetOrAddIndex() const;
+
+  private:
+    UniqueStacks& mUniqueStacks;
+    StackKey mStack;
+  };
+
+  explicit UniqueStacks(JSRuntime* aRuntime);
+  ~UniqueStacks();
+
+  Stack BeginStack(const OnStackFrameKey& aRoot);
+  uint32_t LookupJITFrameDepth(void* aAddr);
+  void AddJITFrameDepth(void* aAddr, unsigned depth);
+  void SpliceFrameTableElements(SpliceableJSONWriter& aWriter) const;
+  void SpliceStackTableElements(SpliceableJSONWriter& aWriter) const;
+
+private:
+  uint32_t GetOrAddFrameIndex(const OnStackFrameKey& aFrame);
+  uint32_t GetOrAddStackIndex(const StackKey& aStack);
+  void StreamFrame(const OnStackFrameKey& aFrame);
+  void StreamStack(const StackKey& aStack);
+
+public:
+  UniqueJSONStrings mUniqueStrings;
+
+private:
+  JSRuntime* mRuntime;
+
+  // To avoid incurring JitcodeGlobalTable lookup costs for every JIT frame,
+  // we cache the depth of frames keyed by JIT code address. If an address a
+  // maps to a depth d, then frames keyed by a for depths 0 to d are
+  // guaranteed to be in mFrameToIndexMap.
+  nsDataHashtable<nsVoidPtrHashKey, uint32_t> mJITFrameDepthMap;
+
+  uint32_t mFrameCount;
+  SpliceableChunkedJSONWriter mFrameTableWriter;
+  nsDataHashtable<nsGenericHashKey<FrameKey>, uint32_t> mFrameToIndexMap;
+
+  SpliceableChunkedJSONWriter mStackTableWriter;
+  nsDataHashtable<nsGenericHashKey<StackKey>, uint32_t> mStackToIndexMap;
 };
 
 class ProfileBuffer {
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ProfileBuffer)
 
   explicit ProfileBuffer(int aEntrySize);
 
   void addTag(const ProfileEntry& aTag);
   void IterateTagsForThread(IterateTagsCallback aCallback, int aThreadId);
-  void StreamSamplesToJSObject(JSStreamWriter& b, int aThreadId, float aSinceTime,
-                               JSRuntime* rt, UniqueJITOptimizations& aUniqueOpts);
-  void StreamMarkersToJSObject(JSStreamWriter& b, int aThreadId, float aSinceTime);
+  void StreamSamplesToJSON(SpliceableJSONWriter& aWriter, int aThreadId, float aSinceTime,
+                           JSRuntime* rt, UniqueStacks& aUniqueStacks);
+  void StreamMarkersToJSON(SpliceableJSONWriter& aWriter, int aThreadId, float aSinceTime,
+                           UniqueStacks& aUniqueStacks);
   void DuplicateLastSample(int aThreadId);
 
   void addStoredMarker(ProfilerMarker* aStoredMarker);
 
   // The following two methods are not signal safe! They delete markers.
   void deleteExpiredStoredMarkers();
   void reset();
 
@@ -135,36 +253,128 @@ public:
 
   // How many times mWritePos has wrapped around.
   uint32_t mGeneration;
 
   // Markers that marker entries in the buffer might refer to.
   ProfilerMarkerLinkedList mStoredMarkers;
 };
 
+//
+// ThreadProfile JSON Format
+// -------------------------
+//
+// The profile contains much duplicate information. The output JSON of the
+// profile attempts to deduplicate strings, frames, and stack prefixes, to cut
+// down on size and to increase JSON streaming speed. Deduplicated values are
+// streamed as indices into their respective tables.
+//
+// Further, arrays of objects with the same set of properties (e.g., samples,
+// frames) are output as arrays according to a schema instead of an object
+// with property names. A property that is not present is represented in the
+// array as null or undefined.
+//
+// The format of the thread profile JSON is shown by the following example
+// with 1 sample and 1 marker:
+//
+// {
+//   "name": "Foo",
+//   "tid": 42,
+//   "samples":
+//   {
+//     "schema":
+//     {
+//       "stack": 0,           /* index into stackTable */
+//       "time": 1,            /* number */
+//       "responsiveness": 2,  /* number */
+//       "rss": 3,             /* number */
+//       "uss": 4,             /* number */
+//       "frameNumber": 5,     /* number */
+//       "power": 6            /* number */
+//     },
+//     "data":
+//     [
+//       [ 1, 0.0, 0.0 ]       /* { stack: 1, time: 0.0, responsiveness: 0.0 } */
+//     ]
+//   },
+//
+//   "markers":
+//   {
+//     "schema":
+//     {
+//       "name": 0,            /* index into stringTable */
+//       "time": 1,            /* number */
+//       "data": 2             /* arbitrary JSON */
+//     },
+//     "data":
+//     [
+//       [ 3, 0.1 ]            /* { name: 'example marker', time: 0.1 } */
+//     ]
+//   },
+//
+//   "stackTable":
+//   {
+//     "schema":
+//     {
+//       "prefix": 0,          /* index into stackTable */
+//       "frame": 1            /* index into frameTable */
+//     },
+//     "data":
+//     [
+//       [ null, 0 ],          /* (root) */
+//       [ 0,    1 ]           /* (root) > foo.js */
+//     ]
+//   },
+//
+//   "frameTable":
+//   {
+//     "schema":
+//     {
+//       "location": 0,        /* index into stringTable */
+//       "implementation": 1,  /* index into stringTable */
+//       "optimizations": 2,   /* arbitrary JSON */
+//       "line": 3,            /* number */
+//       "category": 4         /* number */
+//     },
+//     "data":
+//     [
+//       [ 0 ],                /* { location: '(root)' } */
+//       [ 1, 2 ]              /* { location: 'foo.js', implementation: 'baseline' } */
+//     ]
+//   },
+//
+//   "stringTable":
+//   [
+//     "(root)",
+//     "foo.js",
+//     "baseline",
+//     "example marker"
+//   ]
+// }
+//
+
 class ThreadProfile
 {
 public:
   ThreadProfile(ThreadInfo* aThreadInfo, ProfileBuffer* aBuffer);
   virtual ~ThreadProfile();
   void addTag(const ProfileEntry& aTag);
 
   /**
    * Track a marker which has been inserted into the ThreadProfile.
    * This marker can safely be deleted once the generation has
    * expired.
    */
   void addStoredMarker(ProfilerMarker *aStoredMarker);
-
   void IterateTags(IterateTagsCallback aCallback);
   void ToStreamAsJSON(std::ostream& stream, float aSinceTime = 0);
-  JSObject *ToJSObject(JSContext *aCx, float aSinceTime = 0);
+  JSObject* ToJSObject(JSContext *aCx, float aSinceTime = 0);
   PseudoStack* GetPseudoStack();
   mozilla::Mutex* GetMutex();
-  void StreamJSObject(JSStreamWriter& b, float aSinceTime = 0);
+  void StreamJSON(SpliceableJSONWriter& aWriter, float aSinceTime = 0);
 
   /**
    * Call this method when the JS entries inside the buffer are about to
    * become invalid, i.e., just before JS shutdown.
    */
   void FlushSamplesAndMarkers();
 
   void BeginUnwind();
@@ -186,33 +396,37 @@ public:
     mPseudoStack = nullptr;
     mPlatformData = nullptr;
   }
 
   uint32_t bufferGeneration() const {
     return mBuffer->mGeneration;
   }
 
+protected:
+  void StreamSamplesAndMarkers(SpliceableJSONWriter& aWriter, float aSinceTime,
+                               UniqueStacks& aUniqueStacks);
+
 private:
   FRIEND_TEST(ThreadProfile, InsertOneTag);
   FRIEND_TEST(ThreadProfile, InsertOneTagWithTinyBuffer);
   FRIEND_TEST(ThreadProfile, InsertTagsNoWrap);
   FRIEND_TEST(ThreadProfile, InsertTagsWrap);
   FRIEND_TEST(ThreadProfile, MemoryMeasure);
   ThreadInfo* mThreadInfo;
 
   const nsRefPtr<ProfileBuffer> mBuffer;
 
   // JS frames in the buffer may require a live JSRuntime to stream (e.g.,
   // stringifying JIT frames). In the case of JSRuntime destruction,
   // FlushSamplesAndMarkers should be called to save them. These are spliced
   // into the final stream.
-  std::string mSavedStreamedSamples;
-  std::string mSavedStreamedMarkers;
-  std::string mSavedStreamedOptimizations;
+  mozilla::UniquePtr<char[]> mSavedStreamedSamples;
+  mozilla::UniquePtr<char[]> mSavedStreamedMarkers;
+  mozilla::Maybe<UniqueStacks> mUniqueStacks;
 
   PseudoStack*   mPseudoStack;
   mozilla::Mutex mMutex;
   int            mThreadId;
   bool           mIsMainThread;
   PlatformData*  mPlatformData;  // Platform specific data.
   void* const    mStackTop;
   ThreadResponsiveness mRespInfo;
new file mode 100644
--- /dev/null
+++ b/tools/profiler/ProfileJSONWriter.cpp
@@ -0,0 +1,26 @@
+/* -*- 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/. */
+
+#include "mozilla/HashFunctions.h"
+
+#include "ProfileJSONWriter.h"
+
+void
+SpliceableJSONWriter::Splice(const ChunkedJSONWriteFunc* aFunc)
+{
+  Separator();
+  for (size_t i = 0; i < aFunc->mChunkList.length(); i++) {
+    WriteFunc()->Write(aFunc->mChunkList[i].get());
+  }
+  mNeedComma[mDepth] = true;
+}
+
+void
+SpliceableJSONWriter::Splice(const char* aStr)
+{
+  Separator();
+  WriteFunc()->Write(aStr);
+  mNeedComma[mDepth] = true;
+}
new file mode 100644
--- /dev/null
+++ b/tools/profiler/ProfileJSONWriter.h
@@ -0,0 +1,128 @@
+/* -*- 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 PROFILEJSONWRITER_H
+#define PROFILEJSONWRITER_H
+
+#include <ostream>
+#include <string>
+#include <string.h>
+
+#include "mozilla/JSONWriter.h"
+#include "mozilla/UniquePtr.h"
+
+class SpliceableChunkedJSONWriter;
+
+class ChunkedJSONWriteFunc : public mozilla::JSONWriteFunc
+{
+  friend class SpliceableJSONWriter;
+
+  const static size_t kChunkSize = 4096 * 512;
+  char* mChunkPtr;
+  char* mChunkEnd;
+  mozilla::Vector<mozilla::UniquePtr<char[]>> mChunkList;
+  mozilla::Vector<size_t> mChunkLengths;
+
+  void AllocChunk() {
+    MOZ_ASSERT(mChunkLengths.length() == mChunkList.length());
+    mozilla::UniquePtr<char[]> newChunk = mozilla::MakeUnique<char[]>(kChunkSize);
+    mChunkPtr = newChunk.get();
+    mChunkEnd = mChunkPtr + kChunkSize;
+    MOZ_ALWAYS_TRUE(mChunkLengths.append(0));
+    MOZ_ALWAYS_TRUE(mChunkList.append(mozilla::Move(newChunk)));
+  }
+
+public:
+  ChunkedJSONWriteFunc() {
+    AllocChunk();
+  }
+
+  void Write(const char* aStr) override {
+    MOZ_ASSERT(strlen(aStr) < kChunkSize);
+
+    size_t len = strlen(aStr);
+    char* newPtr = mChunkPtr + len;
+    if (newPtr >= mChunkEnd) {
+      MOZ_ASSERT(*mChunkPtr == '\0');
+      AllocChunk();
+      newPtr = mChunkPtr + len;
+    }
+
+    memcpy(mChunkPtr, aStr, len);
+    mChunkPtr = newPtr;
+    mChunkLengths.back() += len;
+    *mChunkPtr = '\0';
+  }
+
+  mozilla::UniquePtr<char[]> CopyData() {
+    MOZ_ASSERT(mChunkLengths.length() == mChunkList.length());
+    size_t totalLen = 1;
+    for (size_t i = 0; i < mChunkLengths.length(); i++) {
+      MOZ_ASSERT(strlen(mChunkList[i].get()) == mChunkLengths[i]);
+      totalLen += mChunkLengths[i];
+    }
+    mozilla::UniquePtr<char[]> c = mozilla::MakeUnique<char[]>(totalLen);
+    char* ptr = c.get();
+    for (size_t i = 0; i < mChunkList.length(); i++) {
+      size_t len = mChunkLengths[i];
+      memcpy(ptr, mChunkList[i].get(), len);
+      ptr += len;
+    }
+    *ptr = '\0';
+    return c;
+  }
+};
+
+struct OStreamJSONWriteFunc : public mozilla::JSONWriteFunc
+{
+  std::ostream& mStream;
+
+  explicit OStreamJSONWriteFunc(std::ostream& aStream)
+    : mStream(aStream)
+  { }
+
+  void Write(const char* aStr) override {
+    mStream << aStr;
+  }
+};
+
+class SpliceableJSONWriter : public mozilla::JSONWriter
+{
+public:
+  explicit SpliceableJSONWriter(mozilla::UniquePtr<mozilla::JSONWriteFunc> aWriter)
+    : JSONWriter(mozilla::Move(aWriter))
+  { }
+
+  void StartBareList(CollectionStyle aStyle = SingleLineStyle) {
+    StartCollection(nullptr, "", aStyle);
+  }
+
+  void EndBareList() {
+    EndCollection("");
+  }
+
+  void NullElements(uint32_t aCount) {
+    for (uint32_t i = 0; i < aCount; i++) {
+      NullElement();
+    }
+  }
+
+  void Splice(const ChunkedJSONWriteFunc* aFunc);
+  void Splice(const char* aStr);
+};
+
+class SpliceableChunkedJSONWriter : public SpliceableJSONWriter
+{
+public:
+  explicit SpliceableChunkedJSONWriter()
+    : SpliceableJSONWriter(mozilla::MakeUnique<ChunkedJSONWriteFunc>())
+  { }
+
+  ChunkedJSONWriteFunc* WriteFunc() const {
+    return static_cast<ChunkedJSONWriteFunc*>(JSONWriter::WriteFunc());
+  }
+};
+
+#endif // PROFILEJSONWRITER_H
--- a/tools/profiler/ProfilerBacktrace.cpp
+++ b/tools/profiler/ProfilerBacktrace.cpp
@@ -1,17 +1,17 @@
 /* -*- 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 "ProfilerBacktrace.h"
 
-#include "JSStreamWriter.h"
+#include "ProfileJSONWriter.h"
 #include "SyncProfile.h"
 
 ProfilerBacktrace::ProfilerBacktrace(SyncProfile* aProfile)
   : mProfile(aProfile)
 {
   MOZ_COUNT_CTOR(ProfilerBacktrace);
   MOZ_ASSERT(aProfile);
 }
@@ -20,13 +20,14 @@ ProfilerBacktrace::~ProfilerBacktrace()
 {
   MOZ_COUNT_DTOR(ProfilerBacktrace);
   if (mProfile->ShouldDestroy()) {
     delete mProfile;
   }
 }
 
 void
-ProfilerBacktrace::StreamJSObject(JSStreamWriter& b)
+ProfilerBacktrace::StreamJSON(SpliceableJSONWriter& aWriter,
+                              UniqueStacks& aUniqueStacks)
 {
   mozilla::MutexAutoLock lock(*mProfile->GetMutex());
-  mProfile->StreamJSObject(b);
+  mProfile->StreamJSON(aWriter, aUniqueStacks);
 }
--- a/tools/profiler/ProfilerBacktrace.h
+++ b/tools/profiler/ProfilerBacktrace.h
@@ -3,25 +3,32 @@
 /* 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 __PROFILER_BACKTRACE_H
 #define __PROFILER_BACKTRACE_H
 
 class SyncProfile;
-class JSStreamWriter;
+class SpliceableJSONWriter;
+class UniqueStacks;
 
 class ProfilerBacktrace
 {
 public:
   explicit ProfilerBacktrace(SyncProfile* aProfile);
   ~ProfilerBacktrace();
 
-  void StreamJSObject(JSStreamWriter& b);
+  // ProfilerBacktraces' stacks are deduplicated in the context of the
+  // profile that contains the backtrace as a marker payload.
+  //
+  // That is, markers that contain backtraces should not need their own stack,
+  // frame, and string tables. They should instead reuse their parent
+  // profile's tables.
+  void StreamJSON(SpliceableJSONWriter& aWriter, UniqueStacks& aUniqueStacks);
 
 private:
   ProfilerBacktrace(const ProfilerBacktrace&);
   ProfilerBacktrace& operator=(const ProfilerBacktrace&);
 
   SyncProfile*  mProfile;
 };
 
--- a/tools/profiler/ProfilerMarkers.cpp
+++ b/tools/profiler/ProfilerMarkers.cpp
@@ -25,29 +25,33 @@ ProfilerMarkerPayload::ProfilerMarkerPay
 
 ProfilerMarkerPayload::~ProfilerMarkerPayload()
 {
   profiler_free_backtrace(mStack);
 }
 
 void
 ProfilerMarkerPayload::streamCommonProps(const char* aMarkerType,
-                                         JSStreamWriter& b)
+                                         SpliceableJSONWriter& aWriter,
+                                         UniqueStacks& aUniqueStacks)
 {
   MOZ_ASSERT(aMarkerType);
-  b.NameValue("type", aMarkerType);
+  aWriter.StringProperty("type", aMarkerType);
   if (!mStartTime.IsNull()) {
-    b.NameValue("startTime", profiler_time(mStartTime));
+    aWriter.DoubleProperty("startTime", profiler_time(mStartTime));
   }
   if (!mEndTime.IsNull()) {
-    b.NameValue("endTime", profiler_time(mEndTime));
+    aWriter.DoubleProperty("endTime", profiler_time(mEndTime));
   }
   if (mStack) {
-    b.Name("stack");
-    mStack->StreamJSObject(b);
+    aWriter.StartObjectProperty("stack");
+    {
+      mStack->StreamJSON(aWriter, aUniqueStacks);
+    }
+    aWriter.EndObject();
   }
 }
 
 ProfilerMarkerTracing::ProfilerMarkerTracing(const char* aCategory, TracingMetadata aMetaData)
   : mCategory(aCategory)
   , mMetaData(aMetaData)
 {
   if (aMetaData == TRACING_EVENT_BACKTRACE) {
@@ -61,74 +65,69 @@ ProfilerMarkerTracing::ProfilerMarkerTra
   , mMetaData(aMetaData)
 {
   if (aCause) {
     SetStack(aCause);
   }
 }
 
 void
-ProfilerMarkerTracing::streamPayloadImp(JSStreamWriter& b)
+ProfilerMarkerTracing::StreamPayload(SpliceableJSONWriter& aWriter,
+                                     UniqueStacks& aUniqueStacks)
 {
-  b.BeginObject();
-    streamCommonProps("tracing", b);
+  streamCommonProps("tracing", aWriter, aUniqueStacks);
 
-    if (GetCategory()) {
-      b.NameValue("category", GetCategory());
+  if (GetCategory()) {
+    aWriter.StringProperty("category", GetCategory());
+  }
+  if (GetMetaData() != TRACING_DEFAULT) {
+    if (GetMetaData() == TRACING_INTERVAL_START) {
+      aWriter.StringProperty("interval", "start");
+    } else if (GetMetaData() == TRACING_INTERVAL_END) {
+      aWriter.StringProperty("interval", "end");
     }
-    if (GetMetaData() != TRACING_DEFAULT) {
-      if (GetMetaData() == TRACING_INTERVAL_START) {
-        b.NameValue("interval", "start");
-      } else if (GetMetaData() == TRACING_INTERVAL_END) {
-        b.NameValue("interval", "end");
-      }
-    }
-  b.EndObject();
+  }
 }
 
 GPUMarkerPayload::GPUMarkerPayload(
   const mozilla::TimeStamp& aCpuTimeStart,
   const mozilla::TimeStamp& aCpuTimeEnd,
   uint64_t aGpuTimeStart,
   uint64_t aGpuTimeEnd)
 
   : ProfilerMarkerPayload(aCpuTimeStart, aCpuTimeEnd)
   , mCpuTimeStart(aCpuTimeStart)
   , mCpuTimeEnd(aCpuTimeEnd)
   , mGpuTimeStart(aGpuTimeStart)
   , mGpuTimeEnd(aGpuTimeEnd)
-{
-
-}
+{ }
 
 void
-GPUMarkerPayload::streamPayloadImp(JSStreamWriter& b)
+GPUMarkerPayload::StreamPayload(SpliceableJSONWriter& aWriter,
+                                UniqueStacks& aUniqueStacks)
 {
-  b.BeginObject();
-    streamCommonProps("gpu_timer_query", b);
+  streamCommonProps("gpu_timer_query", aWriter, aUniqueStacks);
 
-    b.NameValue("cpustart", profiler_time(mCpuTimeStart));
-    b.NameValue("cpuend", profiler_time(mCpuTimeEnd));
-    b.NameValue("gpustart", (int)mGpuTimeStart);
-    b.NameValue("gpuend", (int)mGpuTimeEnd);
-  b.EndObject();
+  aWriter.DoubleProperty("cpustart", profiler_time(mCpuTimeStart));
+  aWriter.DoubleProperty("cpuend", profiler_time(mCpuTimeEnd));
+  aWriter.IntProperty("gpustart", (int)mGpuTimeStart);
+  aWriter.IntProperty("gpuend", (int)mGpuTimeEnd);
 }
 
 ProfilerMarkerImagePayload::ProfilerMarkerImagePayload(gfxASurface *aImg)
   : mImg(aImg)
-{}
+{ }
 
 void
-ProfilerMarkerImagePayload::streamPayloadImp(JSStreamWriter& b)
+ProfilerMarkerImagePayload::StreamPayload(SpliceableJSONWriter& aWriter,
+                                          UniqueStacks& aUniqueStacks)
 {
-  b.BeginObject();
-    streamCommonProps("innerHTML", b);
-    // TODO: Finish me
-    //b.NameValue("innerHTML", "<img src=''/>");
-  b.EndObject();
+  streamCommonProps("innerHTML", aWriter, aUniqueStacks);
+  // TODO: Finish me
+  //aWriter.NameValue("innerHTML", "<img src=''/>");
 }
 
 IOMarkerPayload::IOMarkerPayload(const char* aSource,
                                  const char* aFilename,
                                  const mozilla::TimeStamp& aStartTime,
                                  const mozilla::TimeStamp& aEndTime,
                                  ProfilerBacktrace* aStack)
   : ProfilerMarkerPayload(aStartTime, aEndTime, aStack),
@@ -138,25 +137,23 @@ IOMarkerPayload::IOMarkerPayload(const c
   MOZ_ASSERT(aSource);
 }
 
 IOMarkerPayload::~IOMarkerPayload(){
   free(mFilename);
 }
 
 void
-IOMarkerPayload::streamPayloadImp(JSStreamWriter& b)
+IOMarkerPayload::StreamPayload(SpliceableJSONWriter& aWriter, UniqueStacks& aUniqueStacks)
 {
-  b.BeginObject();
-    streamCommonProps("io", b);
-    b.NameValue("source", mSource);
-    if (mFilename != nullptr) {
-      b.NameValue("filename", mFilename);
-    }
-  b.EndObject();
+  streamCommonProps("io", aWriter, aUniqueStacks);
+  aWriter.StringProperty("source", mSource);
+  if (mFilename != nullptr) {
+    aWriter.StringProperty("filename", mFilename);
+  }
 }
 
 void
 ProfilerJSEventMarker(const char *event)
 {
     PROFILER_MARKER(event);
 }
 
@@ -164,51 +161,46 @@ LayerTranslationPayload::LayerTranslatio
                                                  mozilla::gfx::Point aPoint)
   : ProfilerMarkerPayload(mozilla::TimeStamp::Now(), mozilla::TimeStamp::Now(), nullptr)
   , mLayer(aLayer)
   , mPoint(aPoint)
 {
 }
 
 void
-LayerTranslationPayload::streamPayloadImpl(JSStreamWriter& b)
+LayerTranslationPayload::StreamPayload(SpliceableJSONWriter& aWriter,
+                                       UniqueStacks& aUniqueStacks)
 {
   const size_t bufferSize = 32;
   char buffer[bufferSize];
   PR_snprintf(buffer, bufferSize, "%p", mLayer);
 
-  b.BeginObject();
-  b.NameValue("layer", buffer);
-  b.NameValue("x", mPoint.x);
-  b.NameValue("y", mPoint.y);
-  b.NameValue("category", "LayerTranslation");
-  b.EndObject();
+  aWriter.StringProperty("layer", buffer);
+  aWriter.IntProperty("x", mPoint.x);
+  aWriter.IntProperty("y", mPoint.y);
+  aWriter.StringProperty("category", "LayerTranslation");
 }
 
 TouchDataPayload::TouchDataPayload(const mozilla::ScreenIntPoint& aPoint)
   : ProfilerMarkerPayload(mozilla::TimeStamp::Now(), mozilla::TimeStamp::Now(), nullptr)
 {
   mPoint = aPoint;
 }
 
 void
-TouchDataPayload::streamPayloadImpl(JSStreamWriter& b)
+TouchDataPayload::StreamPayload(SpliceableJSONWriter& aWriter, UniqueStacks& aUniqueStacks)
 {
-  b.BeginObject();
-  b.NameValue("x", mPoint.x);
-  b.NameValue("y", mPoint.y);
-  b.EndObject();
+  aWriter.IntProperty("x", mPoint.x);
+  aWriter.IntProperty("y", mPoint.y);
 }
 
 VsyncPayload::VsyncPayload(mozilla::TimeStamp aVsyncTimestamp)
   : ProfilerMarkerPayload(aVsyncTimestamp, aVsyncTimestamp, nullptr)
   , mVsyncTimestamp(aVsyncTimestamp)
 {
 }
 
 void
-VsyncPayload::streamPayloadImpl(JSStreamWriter& b)
+VsyncPayload::StreamPayload(SpliceableJSONWriter& aWriter, UniqueStacks& aUniqueStacks)
 {
-  b.BeginObject();
-  b.NameValue("vsync", profiler_time(mVsyncTimestamp));
-  b.NameValue("category", "VsyncTimestamp");
-  b.EndObject();
+  aWriter.DoubleProperty("vsync", profiler_time(mVsyncTimestamp));
+  aWriter.StringProperty("category", "VsyncTimestamp");
 }
--- a/tools/profiler/ProfilerMarkers.h
+++ b/tools/profiler/ProfilerMarkers.h
@@ -1,28 +1,30 @@
 /* -*- 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 PROFILER_MARKERS_H
 #define PROFILER_MARKERS_H
 
-#include "JSStreamWriter.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/Attributes.h"
 #include "nsAutoPtr.h"
 #include "Units.h"    // For ScreenIntPoint
 
 namespace mozilla {
 namespace layers {
 class Layer;
 } // layers
 } // mozilla
 
+class SpliceableJSONWriter;
+class UniqueStacks;
+
 /**
  * This is an abstract object that can be implied to supply
  * data to be attached with a profiler marker. Most data inserted
  * into a profile is stored in a circular buffer. This buffer
  * typically wraps around and overwrites most entries. Because
  * of this, this structure is designed to defer the work of
  * prepare the payload only when 'preparePayload' is called.
  *
@@ -44,33 +46,27 @@ public:
   /**
    * Called from the main thread
    */
   virtual ~ProfilerMarkerPayload();
 
   /**
    * Called from the main thread
    */
-  void StreamPayload(JSStreamWriter& b) {
-    return streamPayload(b);
-  }
+  virtual void StreamPayload(SpliceableJSONWriter& aWriter,
+                             UniqueStacks& aUniqueStacks) = 0;
 
   mozilla::TimeStamp GetStartTime() const { return mStartTime; }
 
 protected:
   /**
    * Called from the main thread
    */
-  void streamCommonProps(const char* aMarkerType, JSStreamWriter& b);
-
-  /**
-   * Called from the main thread
-   */
-  virtual void
-  streamPayload(JSStreamWriter& b) = 0;
+  void streamCommonProps(const char* aMarkerType, SpliceableJSONWriter& aWriter,
+                         UniqueStacks& aUniqueStacks);
 
   void SetStack(ProfilerBacktrace* aStack) { mStack = aStack; }
 
 private:
   mozilla::TimeStamp  mStartTime;
   mozilla::TimeStamp  mEndTime;
   ProfilerBacktrace*  mStack;
 };
@@ -79,136 +75,117 @@ class ProfilerMarkerTracing : public Pro
 {
 public:
   ProfilerMarkerTracing(const char* aCategory, TracingMetadata aMetaData);
   ProfilerMarkerTracing(const char* aCategory, TracingMetadata aMetaData, ProfilerBacktrace* aCause);
 
   const char *GetCategory() const { return mCategory; }
   TracingMetadata GetMetaData() const { return mMetaData; }
 
-protected:
-  virtual void
-  streamPayload(JSStreamWriter& b) { return streamPayloadImp(b); }
-
-private:
-  void streamPayloadImp(JSStreamWriter& b);
+  virtual void StreamPayload(SpliceableJSONWriter& aWriter,
+                             UniqueStacks& aUniqueStacks) override;
 
 private:
   const char *mCategory;
   TracingMetadata mMetaData;
 };
 
 
 #include "gfxASurface.h"
 class ProfilerMarkerImagePayload : public ProfilerMarkerPayload
 {
 public:
   explicit ProfilerMarkerImagePayload(gfxASurface *aImg);
 
-protected:
-  virtual void
-  streamPayload(JSStreamWriter& b) { return streamPayloadImp(b); }
+  virtual void StreamPayload(SpliceableJSONWriter& aWriter,
+                             UniqueStacks& aUniqueStacks) override;
 
 private:
-  void streamPayloadImp(JSStreamWriter& b);
-
   nsRefPtr<gfxASurface> mImg;
 };
 
 class IOMarkerPayload : public ProfilerMarkerPayload
 {
 public:
   IOMarkerPayload(const char* aSource, const char* aFilename, const mozilla::TimeStamp& aStartTime,
                   const mozilla::TimeStamp& aEndTime,
                   ProfilerBacktrace* aStack);
   ~IOMarkerPayload();
 
-protected:
-  virtual void
-  streamPayload(JSStreamWriter& b) { return streamPayloadImp(b); }
+  virtual void StreamPayload(SpliceableJSONWriter& aWriter,
+                             UniqueStacks& aUniqueStacks) override;
 
 private:
-  void streamPayloadImp(JSStreamWriter& b);
-
   const char* mSource;
   char* mFilename;
 };
 
 /**
  * Contains the translation applied to a 2d layer so we can
  * track the layer position at each frame.
  */
 class LayerTranslationPayload : public ProfilerMarkerPayload
 {
 public:
   LayerTranslationPayload(mozilla::layers::Layer* aLayer,
                           mozilla::gfx::Point aPoint);
 
-protected:
-  virtual void
-  streamPayload(JSStreamWriter& b) { return streamPayloadImpl(b); }
+  virtual void StreamPayload(SpliceableJSONWriter& aWriter,
+                             UniqueStacks& aUniqueStacks) override;
 
 private:
-  void streamPayloadImpl(JSStreamWriter& b);
   mozilla::layers::Layer* mLayer;
   mozilla::gfx::Point mPoint;
 };
 
 /**
  * Tracks when touch events are processed by gecko, not when
  * the touch actually occured in gonk/android.
  */
 class TouchDataPayload : public ProfilerMarkerPayload
 {
 public:
   explicit TouchDataPayload(const mozilla::ScreenIntPoint& aPoint);
   virtual ~TouchDataPayload() {}
 
-protected:
-  virtual void
-  streamPayload(JSStreamWriter& b) { return streamPayloadImpl(b); }
+  virtual void StreamPayload(SpliceableJSONWriter& aWriter,
+                             UniqueStacks& aUniqueStacks) override;
 
 private:
-  void streamPayloadImpl(JSStreamWriter& b);
   mozilla::ScreenIntPoint mPoint;
 };
 
 /**
  * Tracks when a vsync occurs according to the HardwareComposer.
  */
 class VsyncPayload : public ProfilerMarkerPayload
 {
 public:
   explicit VsyncPayload(mozilla::TimeStamp aVsyncTimestamp);
   virtual ~VsyncPayload() {}
 
-protected:
-  virtual void
-  streamPayload(JSStreamWriter& b) { return streamPayloadImpl(b); }
+  virtual void StreamPayload(SpliceableJSONWriter& aWriter,
+                             UniqueStacks& aUniqueStacks) override;
 
 private:
-  void streamPayloadImpl(JSStreamWriter& b);
   mozilla::TimeStamp mVsyncTimestamp;
 };
 
 class GPUMarkerPayload : public ProfilerMarkerPayload
 {
 public:
   GPUMarkerPayload(const mozilla::TimeStamp& aCpuTimeStart,
                    const mozilla::TimeStamp& aCpuTimeEnd,
                    uint64_t aGpuTimeStart,
                    uint64_t aGpuTimeEnd);
   ~GPUMarkerPayload() {}
 
-protected:
-  virtual void
-  streamPayload(JSStreamWriter& b) override { return streamPayloadImp(b); }
+  virtual void StreamPayload(SpliceableJSONWriter& aWriter,
+                             UniqueStacks& aUniqueStacks) override;
 
 private:
-  void streamPayloadImp(JSStreamWriter& b);
-
   mozilla::TimeStamp mCpuTimeStart;
   mozilla::TimeStamp mCpuTimeEnd;
   uint64_t mGpuTimeStart;
   uint64_t mGpuTimeEnd;
 };
 
 #endif // PROFILER_MARKERS_H
--- a/tools/profiler/PseudoStack.h
+++ b/tools/profiler/PseudoStack.h
@@ -69,32 +69,33 @@ static inline uint32_t sMin(uint32_t l, 
 // of the two representations are consistent.
 class StackEntry : public js::ProfileEntry
 {
 };
 
 class ProfilerMarkerPayload;
 template<typename T>
 class ProfilerLinkedList;
-class JSStreamWriter;
+class SpliceableJSONWriter;
+class UniqueStacks;
+
 class ProfilerMarker {
   friend class ProfilerLinkedList<ProfilerMarker>;
 public:
   explicit ProfilerMarker(const char* aMarkerName,
-         ProfilerMarkerPayload* aPayload = nullptr,
-         float aTime = 0);
+                          ProfilerMarkerPayload* aPayload = nullptr,
+                          float aTime = 0);
 
   ~ProfilerMarker();
 
   const char* GetMarkerName() const {
     return mMarkerName;
   }
 
-  void
-  StreamJSObject(JSStreamWriter& b) const;
+  void StreamJSON(SpliceableJSONWriter& aWriter, UniqueStacks& aUniqueStacks) const;
 
   void SetGeneration(uint32_t aGenID);
 
   bool HasExpired(uint32_t aGenID) const {
     return mGenID + 2 <= aGenID;
   }
 
   float GetTime() const;
--- a/tools/profiler/SyncProfile.cpp
+++ b/tools/profiler/SyncProfile.cpp
@@ -46,8 +46,15 @@ SyncProfile::EndUnwind()
   // Save mOwnerState before we release the mutex
   OwnerState ownerState = mOwnerState;
   ThreadProfile::EndUnwind();
   if (ownerState == ORPHANED) {
     delete this;
   }
 }
 
+// SyncProfiles' stacks are deduplicated in the context of the containing
+// profile in which the backtrace is as a marker payload.
+void
+SyncProfile::StreamJSON(SpliceableJSONWriter& aWriter, UniqueStacks& aUniqueStacks)
+{
+  ThreadProfile::StreamSamplesAndMarkers(aWriter, /* aSinceTime = */ 0, aUniqueStacks);
+}
--- a/tools/profiler/SyncProfile.h
+++ b/tools/profiler/SyncProfile.h
@@ -10,16 +10,20 @@
 #include "ProfileEntry.h"
 
 class SyncProfile : public ThreadProfile
 {
 public:
   SyncProfile(ThreadInfo* aInfo, int aEntrySize);
   ~SyncProfile();
 
+  // SyncProfiles' stacks are deduplicated in the context of the containing
+  // profile in which the backtrace is as a marker payload.
+  void StreamJSON(SpliceableJSONWriter& aWriter, UniqueStacks& aUniqueStacks);
+
   virtual void EndUnwind();
   virtual SyncProfile* AsSyncProfile() { return this; }
 
 private:
   friend class ProfilerBacktrace;
 
   enum OwnerState
   {
--- a/tools/profiler/TableTicker.cpp
+++ b/tools/profiler/TableTicker.cpp
@@ -17,17 +17,17 @@
 #include "prenv.h"
 #include "prtime.h"
 #include "shared-libraries.h"
 #include "mozilla/StackWalk.h"
 #include "TableTicker.h"
 #include "nsXULAppAPI.h"
 
 // JSON
-#include "JSStreamWriter.h"
+#include "ProfileJSONWriter.h"
 
 // Meta
 #include "nsXPCOM.h"
 #include "nsXPCOMCID.h"
 #include "nsIHttpProtocolHandler.h"
 #include "nsServiceManagerUtils.h"
 #include "nsIXULRuntime.h"
 #include "nsIXULAppInfo.h"
@@ -120,271 +120,255 @@ void TableTicker::HandleSaveRequest()
   NS_DispatchToMainThread(runnable);
 }
 
 void TableTicker::DeleteExpiredMarkers()
 {
   mBuffer->deleteExpiredStoredMarkers();
 }
 
-void TableTicker::StreamTaskTracer(JSStreamWriter& b)
+void TableTicker::StreamTaskTracer(SpliceableJSONWriter& aWriter)
 {
-  b.BeginObject();
 #ifdef MOZ_TASK_TRACER
-    b.Name("data");
-    b.BeginArray();
-      nsAutoPtr<nsTArray<nsCString>> data(
-        mozilla::tasktracer::GetLoggedData(sStartTime));
-      for (uint32_t i = 0; i < data->Length(); ++i) {
-        b.Value((data->ElementAt(i)).get());
-      }
-    b.EndArray();
+  aWriter.StartArrayProperty("data");
+    nsAutoPtr<nsTArray<nsCString>> data(mozilla::tasktracer::GetLoggedData(sStartTime));
+    for (uint32_t i = 0; i < data->Length(); ++i) {
+      aWriter.StringElement((data->ElementAt(i)).get());
+    }
+  aWriter.EndArray();
 
-    b.Name("threads");
-    b.BeginArray();
-      mozilla::MutexAutoLock lock(*sRegisteredThreadsMutex);
-      for (size_t i = 0; i < sRegisteredThreads->size(); i++) {
-        // Thread meta data
-        ThreadInfo* info = sRegisteredThreads->at(i);
-        b.BeginObject();
+  aWriter.StartArrayProperty("threads");
+    mozilla::MutexAutoLock lock(*sRegisteredThreadsMutex);
+    for (size_t i = 0; i < sRegisteredThreads->size(); i++) {
+      // Thread meta data
+      ThreadInfo* info = sRegisteredThreads->at(i);
+      aWriter.StartObjectElement();
         if (XRE_GetProcessType() == GeckoProcessType_Plugin) {
           // TODO Add the proper plugin name
-          b.NameValue("name", "Plugin");
+          aWriter.StringProperty("name", "Plugin");
         } else {
-          b.NameValue("name", info->Name());
+          aWriter.StringProperty("name", info->Name());
         }
-        b.NameValue("tid", static_cast<int>(info->ThreadId()));
-        b.EndObject();
-      }
-    b.EndArray();
+        aWriter.IntProperty("tid", static_cast<int>(info->ThreadId()));
+      aWriter.EndObject();
+    }
+  aWriter.EndArray();
 
-    b.NameValue("start",
-                static_cast<double>(mozilla::tasktracer::GetStartTime()));
+  aWriter.DoubleProperty("start", static_cast<double>(mozilla::tasktracer::GetStartTime()));
 #endif
-  b.EndObject();
 }
 
 
-void TableTicker::StreamMetaJSCustomObject(JSStreamWriter& b)
+void TableTicker::StreamMetaJSCustomObject(SpliceableJSONWriter& aWriter)
 {
-  b.BeginObject();
+  aWriter.IntProperty("version", 3);
+  aWriter.DoubleProperty("interval", interval());
+  aWriter.IntProperty("stackwalk", mUseStackWalk);
+  aWriter.IntProperty("processType", XRE_GetProcessType());
 
-    b.NameValue("version", 2);
-    b.NameValue("interval", interval());
-    b.NameValue("stackwalk", mUseStackWalk);
-    b.NameValue("processType", XRE_GetProcessType());
-
-    mozilla::TimeDuration delta = mozilla::TimeStamp::Now() - sStartTime;
-    b.NameValue("startTime", static_cast<double>(PR_Now()/1000.0 - delta.ToMilliseconds()));
+  mozilla::TimeDuration delta = mozilla::TimeStamp::Now() - sStartTime;
+  aWriter.DoubleProperty("startTime", static_cast<double>(PR_Now()/1000.0 - delta.ToMilliseconds()));
 
-    nsresult res;
-    nsCOMPtr<nsIHttpProtocolHandler> http = do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "http", &res);
-    if (!NS_FAILED(res)) {
-      nsAutoCString string;
+  nsresult res;
+  nsCOMPtr<nsIHttpProtocolHandler> http = do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "http", &res);
+  if (!NS_FAILED(res)) {
+    nsAutoCString string;
 
-      res = http->GetPlatform(string);
-      if (!NS_FAILED(res))
-        b.NameValue("platform", string.Data());
+    res = http->GetPlatform(string);
+    if (!NS_FAILED(res))
+      aWriter.StringProperty("platform", string.Data());
 
-      res = http->GetOscpu(string);
-      if (!NS_FAILED(res))
-        b.NameValue("oscpu", string.Data());
+    res = http->GetOscpu(string);
+    if (!NS_FAILED(res))
+      aWriter.StringProperty("oscpu", string.Data());
 
-      res = http->GetMisc(string);
-      if (!NS_FAILED(res))
-        b.NameValue("misc", string.Data());
-    }
+    res = http->GetMisc(string);
+    if (!NS_FAILED(res))
+      aWriter.StringProperty("misc", string.Data());
+  }
 
-    nsCOMPtr<nsIXULRuntime> runtime = do_GetService("@mozilla.org/xre/runtime;1");
-    if (runtime) {
-      nsAutoCString string;
+  nsCOMPtr<nsIXULRuntime> runtime = do_GetService("@mozilla.org/xre/runtime;1");
+  if (runtime) {
+    nsAutoCString string;
 
-      res = runtime->GetXPCOMABI(string);
-      if (!NS_FAILED(res))
-        b.NameValue("abi", string.Data());
+    res = runtime->GetXPCOMABI(string);
+    if (!NS_FAILED(res))
+      aWriter.StringProperty("abi", string.Data());
 
-      res = runtime->GetWidgetToolkit(string);
-      if (!NS_FAILED(res))
-        b.NameValue("toolkit", string.Data());
-    }
+    res = runtime->GetWidgetToolkit(string);
+    if (!NS_FAILED(res))
+      aWriter.StringProperty("toolkit", string.Data());
+  }
 
-    nsCOMPtr<nsIXULAppInfo> appInfo = do_GetService("@mozilla.org/xre/app-info;1");
-    if (appInfo) {
-      nsAutoCString string;
+  nsCOMPtr<nsIXULAppInfo> appInfo = do_GetService("@mozilla.org/xre/app-info;1");
+  if (appInfo) {
+    nsAutoCString string;
 
-      res = appInfo->GetName(string);
-      if (!NS_FAILED(res))
-        b.NameValue("product", string.Data());
-    }
-
-  b.EndObject();
+    res = appInfo->GetName(string);
+    if (!NS_FAILED(res))
+      aWriter.StringProperty("product", string.Data());
+  }
 }
 
 void TableTicker::ToStreamAsJSON(std::ostream& stream, float aSinceTime)
 {
-  JSStreamWriter b(stream);
-  StreamJSObject(b, aSinceTime);
+  SpliceableJSONWriter b(mozilla::MakeUnique<OStreamJSONWriteFunc>(stream));
+  StreamJSON(b, aSinceTime);
 }
 
 JSObject* TableTicker::ToJSObject(JSContext *aCx, float aSinceTime)
 {
   JS::RootedValue val(aCx);
-  std::stringstream ss;
   {
-    // Define a scope to prevent a moving GC during ~JSStreamWriter from
-    // trashing the return value.
-    JSStreamWriter b(ss);
-    StreamJSObject(b, aSinceTime);
-    NS_ConvertUTF8toUTF16 js_string(nsDependentCString(ss.str().c_str()));
-    JS_ParseJSON(aCx, static_cast<const char16_t*>(js_string.get()),
-                 js_string.Length(), &val);
+    SpliceableChunkedJSONWriter b;
+    StreamJSON(b, aSinceTime);
+    UniquePtr<char[]> buf = b.WriteFunc()->CopyData();
+    NS_ConvertUTF8toUTF16 js_string(nsDependentCString(buf.get()));
+    MOZ_ALWAYS_TRUE(JS_ParseJSON(aCx, static_cast<const char16_t*>(js_string.get()),
+                                 js_string.Length(), &val));
   }
   return &val.toObject();
 }
 
 struct SubprocessClosure {
-  explicit SubprocessClosure(JSStreamWriter *aWriter)
+  explicit SubprocessClosure(SpliceableJSONWriter *aWriter)
     : mWriter(aWriter)
   {}
 
-  JSStreamWriter* mWriter;
+  SpliceableJSONWriter* mWriter;
 };
 
 void SubProcessCallback(const char* aProfile, void* aClosure)
 {
   // Called by the observer to get their profile data included
   // as a sub profile
   SubprocessClosure* closure = (SubprocessClosure*)aClosure;
 
   // Add the string profile into the profile
-  closure->mWriter->Value(aProfile);
+  closure->mWriter->StringElement(aProfile);
 }
 
 
 #if defined(SPS_OS_android) && !defined(MOZ_WIDGET_GONK)
 static
-void BuildJavaThreadJSObject(JSStreamWriter& b)
+void BuildJavaThreadJSObject(SpliceableJSONWriter& aWriter)
 {
-  b.BeginObject();
+  aWriter.StringProperty("name", "Java Main Thread");
 
-    b.NameValue("name", "Java Main Thread");
-
-    b.Name("samples");
-    b.BeginArray();
+  aWriter.StartArrayProperty("samples");
 
-      // for each sample
-      for (int sampleId = 0; true; sampleId++) {
-        bool firstRun = true;
-        // for each frame
-        for (int frameId = 0; true; frameId++) {
-          nsCString result;
-          bool hasFrame = AndroidBridge::Bridge()->GetFrameNameJavaProfiling(0, sampleId, frameId, result);
-          // when we run out of frames, we stop looping
-          if (!hasFrame) {
-            // if we found at least one frame, we have objects to close
-            if (!firstRun) {
-                b.EndArray();
-              b.EndObject();
-            }
-            break;
+    // for each sample
+    for (int sampleId = 0; true; sampleId++) {
+      bool firstRun = true;
+      // for each frame
+      for (int frameId = 0; true; frameId++) {
+        nsCString result;
+        bool hasFrame = AndroidBridge::Bridge()->GetFrameNameJavaProfiling(0, sampleId, frameId, result);
+        // when we run out of frames, we stop looping
+        if (!hasFrame) {
+          // if we found at least one frame, we have objects to close
+          if (!firstRun) {
+              aWriter.EndArray();
+            aWriter.EndObject();
           }
-          // the first time around, open the sample object and frames array
-          if (firstRun) {
-            firstRun = false;
-
-            double sampleTime =
-              mozilla::widget::GeckoJavaSampler::GetSampleTimeJavaProfiling(0, sampleId);
-
-            b.BeginObject();
-              b.NameValue("time", sampleTime);
-
-              b.Name("frames");
-              b.BeginArray();
-          }
-          // add a frame to the sample
-          b.BeginObject();
-            b.NameValue("location", result.BeginReading());
-          b.EndObject();
-        }
-        // if we found no frames for this sample, we are done
-        if (firstRun) {
           break;
         }
-      }
+        // the first time around, open the sample object and frames array
+        if (firstRun) {
+          firstRun = false;
+
+          double sampleTime =
+            mozilla::widget::GeckoJavaSampler::GetSampleTimeJavaProfiling(0, sampleId);
+
+          aWriter.StartObjectElement();
+            aWriter.DoubleProperty("time", sampleTime);
 
-    b.EndArray();
+            aWriter.StartArrayProperty("frames");
+        }
+        // add a frame to the sample
+        aWriter.StartObjectElement();
+          aWriter.StringProperty("location", result.BeginReading());
+        aWriter.EndObject();
+      }
+      // if we found no frames for this sample, we are done
+      if (firstRun) {
+        break;
+      }
+    }
 
-  b.EndObject();
+  aWriter.EndArray();
 }
 #endif
 
-void TableTicker::StreamJSObject(JSStreamWriter& b, float aSinceTime)
+void TableTicker::StreamJSON(SpliceableJSONWriter& aWriter, float aSinceTime)
 {
-  b.BeginObject();
+  aWriter.Start(SpliceableJSONWriter::SingleLineStyle);
+  {
     // Put shared library info
-    b.NameValue("libs", GetSharedLibraryInfoString().c_str());
+    aWriter.StringProperty("libs", GetSharedLibraryInfoString().c_str());
 
     // Put meta data
-    b.Name("meta");
-    StreamMetaJSCustomObject(b);
+    aWriter.StartObjectProperty("meta");
+      StreamMetaJSCustomObject(aWriter);
+    aWriter.EndObject();
 
     // Data of TaskTracer doesn't belong in the circular buffer.
     if (TaskTracer()) {
-      b.Name("tasktracer");
-      StreamTaskTracer(b);
+      aWriter.StartObjectProperty("tasktracer");
+      StreamTaskTracer(aWriter);
     }
 
     // Lists the samples for each ThreadProfile
-    b.Name("threads");
-    b.BeginArray();
-
+    aWriter.StartArrayProperty("threads");
+    {
       SetPaused(true);
 
       {
         mozilla::MutexAutoLock lock(*sRegisteredThreadsMutex);
 
         for (size_t i = 0; i < sRegisteredThreads->size(); i++) {
           // Thread not being profiled, skip it
           if (!sRegisteredThreads->at(i)->Profile())
             continue;
 
           // Note that we intentionally include ThreadProfile which
           // have been marked for pending delete.
 
           MutexAutoLock lock(*sRegisteredThreads->at(i)->Profile()->GetMutex());
 
-          sRegisteredThreads->at(i)->Profile()->StreamJSObject(b, aSinceTime);
+          sRegisteredThreads->at(i)->Profile()->StreamJSON(aWriter, aSinceTime);
         }
       }
 
       if (Sampler::CanNotifyObservers()) {
         // Send a event asking any subprocesses (plugins) to
         // give us their information
-        SubprocessClosure closure(&b);
+        SubprocessClosure closure(&aWriter);
         nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
         if (os) {
           nsRefPtr<ProfileSaveEvent> pse = new ProfileSaveEvent(SubProcessCallback, &closure);
           os->NotifyObservers(pse, "profiler-subprocess", nullptr);
         }
       }
 
   #if defined(SPS_OS_android) && !defined(MOZ_WIDGET_GONK)
       if (ProfileJava()) {
         mozilla::widget::GeckoJavaSampler::PauseJavaProfiling();
 
-        BuildJavaThreadJSObject(b);
+        BuildJavaThreadJSObject(aWriter);
 
         mozilla::widget::GeckoJavaSampler::UnpauseJavaProfiling();
       }
   #endif
 
       SetPaused(false);
-    b.EndArray();
-
-  b.EndObject();
+    }
+    aWriter.EndArray();
+  }
+  aWriter.End();
 }
 
 void TableTicker::FlushOnJSShutdown(JSRuntime* aRuntime)
 {
   SetPaused(true);
 
   {
     mozilla::MutexAutoLock lock(*sRegisteredThreadsMutex);
--- a/tools/profiler/TableTicker.h
+++ b/tools/profiler/TableTicker.h
@@ -189,18 +189,18 @@ class TableTicker: public Sampler {
       }
     }
 
     return mPrimaryThreadProfile;
   }
 
   void ToStreamAsJSON(std::ostream& stream, float aSinceTime = 0);
   virtual JSObject *ToJSObject(JSContext *aCx, float aSinceTime = 0);
-  void StreamMetaJSCustomObject(JSStreamWriter& b);
-  void StreamTaskTracer(JSStreamWriter& b);
+  void StreamMetaJSCustomObject(SpliceableJSONWriter& aWriter);
+  void StreamTaskTracer(SpliceableJSONWriter& aWriter);
   void FlushOnJSShutdown(JSRuntime* aRuntime);
   bool ProfileJS() const { return mProfileJS; }
   bool ProfileJava() const { return mProfileJava; }
   bool ProfileGPU() const { return mProfileGPU; }
   bool ProfilePower() const { return mProfilePower; }
   bool ProfileThreads() const override { return mProfileThreads; }
   bool InPrivacyMode() const { return mPrivacyMode; }
   bool AddMainThreadIO() const { return mAddMainThreadIO; }
@@ -214,17 +214,17 @@ class TableTicker: public Sampler {
 
 protected:
   // Called within a signal. This function must be reentrant
   virtual void InplaceTick(TickSample* sample);
 
   // Not implemented on platforms which do not support backtracing
   void doNativeBacktrace(ThreadProfile &aProfile, TickSample* aSample);
 
-  void StreamJSObject(JSStreamWriter& b, float aSinceTime);
+  void StreamJSON(SpliceableJSONWriter& aWriter, float aSinceTime);
 
   // This represent the application's main thread (SAMPLER_INIT)
   ThreadProfile* mPrimaryThreadProfile;
   nsRefPtr<ProfileBuffer> mBuffer;
   bool mSaveRequested;
   bool mAddLeafAddresses;
   bool mUseStackWalk;
   bool mProfileJS;
--- a/tools/profiler/moz.build
+++ b/tools/profiler/moz.build
@@ -10,32 +10,31 @@ if CONFIG['MOZ_ENABLE_PROFILER_SPS']:
     XPIDL_MODULE = 'profiler'
     XPIDL_SOURCES += [
         'nsIProfiler.idl',
         'nsIProfileSaveEvent.idl',
     ]
     EXPORTS += [
         'GeckoProfilerFunc.h',
         'GeckoProfilerImpl.h',
-        'JSStreamWriter.h',
         'ProfilerBacktrace.h',
         'ProfilerMarkers.h',
         'PseudoStack.h',
         'shared-libraries.h',
     ]
     EXTRA_JS_MODULES += [
         'Profiler.jsm',
     ]
     UNIFIED_SOURCES += [
-        'JSStreamWriter.cpp',
         'nsProfiler.cpp',
         'nsProfilerFactory.cpp',
         'nsProfilerStartParams.cpp',
         'platform.cpp',
         'ProfileEntry.cpp',
+        'ProfileJSONWriter.cpp',
         'ProfilerBacktrace.cpp',
         'ProfilerIOInterposeObserver.cpp',
         'ProfilerMarkers.cpp',
         'SaveProfileTask.cpp',
         'SyncProfile.cpp',
         'TableTicker.cpp',
         'ThreadResponsiveness.cpp',
     ]
--- a/tools/profiler/platform.cpp
+++ b/tools/profiler/platform.cpp
@@ -200,28 +200,38 @@ ProfilerMarker::SetGeneration(uint32_t a
   mGenID = aGenID;
 }
 
 float
 ProfilerMarker::GetTime() const {
   return mTime;
 }
 
-void ProfilerMarker::StreamJSObject(JSStreamWriter& b) const {
-  b.BeginObject();
-    b.NameValue("name", GetMarkerName());
+void ProfilerMarker::StreamJSON(SpliceableJSONWriter& aWriter,
+                                UniqueStacks& aUniqueStacks) const
+{
+  // Schema:
+  //   [name, time, data]
+
+  aWriter.StartArrayElement();
+  {
+    aUniqueStacks.mUniqueStrings.WriteElement(aWriter, GetMarkerName());
+    aWriter.DoubleElement(mTime);
     // TODO: Store the callsite for this marker if available:
     // if have location data
     //   b.NameValue(marker, "location", ...);
     if (mPayload) {
-      b.Name("data");
-      mPayload->StreamPayload(b);
+      aWriter.StartObjectElement();
+      {
+          mPayload->StreamPayload(aWriter, aUniqueStacks);
+      }
+      aWriter.EndObject();
     }
-    b.NameValue("time", mTime);
-  b.EndObject();
+  }
+  aWriter.EndArray();
 }
 
 /* Has MOZ_PROFILER_VERBOSE been set? */
 
 // Verbosity control for the profiler.  The aim is to check env var
 // MOZ_PROFILER_VERBOSE only once.  However, we may need to temporarily
 // override that so as to print the profiler's help message.  That's
 // what moz_profiler_set_verbosity is for.
deleted file mode 100644
--- a/tools/profiler/tests/gtest/JSStreamWriterTest.cpp
+++ /dev/null
@@ -1,183 +0,0 @@
-/* -*- 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/. */
-
-#include "gtest/gtest.h"
-
-#include <sstream>
-#include "JSStreamWriter.h"
-
-TEST(JSStreamWriter, NoOutput) {
-  std::stringstream ss;
-  JSStreamWriter b(ss);
-  ASSERT_TRUE(ss.str().compare("") == 0);
-}
-
-TEST(JSStreamWriter, EmptyObject) {
-  std::stringstream ss;
-  JSStreamWriter b(ss);
-  b.BeginObject();
-  b.EndObject();
-  ASSERT_TRUE(ss.str().compare("{}") == 0);
-}
-
-TEST(JSStreamWriter, OnePropertyObject) {
-  std::stringstream ss;
-  JSStreamWriter b(ss);
-  b.BeginObject();
-  b.Name("a");
-  b.Value(1);
-  b.EndObject();
-  ASSERT_TRUE(ss.str().compare("{\"a\":1}") == 0);
-}
-
-TEST(JSStreamWriter, MultiPropertyObject) {
-  std::stringstream ss;
-  JSStreamWriter b(ss);
-  b.BeginObject();
-  b.Name("a");
-  b.Value(1);
-  b.Name("b");
-  b.Value(2);
-  b.EndObject();
-  ASSERT_TRUE(ss.str().compare("{\"a\":1,\"b\":2}") == 0);
-}
-
-TEST(JSStreamWriter, OnePropertyArray) {
-  std::stringstream ss;
-  JSStreamWriter b(ss);
-  b.BeginArray();
-  b.Value(1);
-  b.EndArray();
-  ASSERT_TRUE(ss.str().compare("[1]") == 0);
-}
-
-TEST(JSStreamWriter, MultiPropertyArray) {
-  std::stringstream ss;
-  JSStreamWriter b(ss);
-  b.BeginArray();
-  b.Value(1);
-  b.Value(2);
-  b.EndArray();
-  ASSERT_TRUE(ss.str().compare("[1,2]") == 0);
-}
-
-TEST(JSStreamWriter, NestedObject) {
-  std::stringstream ss;
-  JSStreamWriter b(ss);
-  b.BeginObject();
-  b.Name("a");
-  b.BeginObject();
-  b.Name("b");
-  b.Value(1);
-  b.EndObject();
-  b.EndObject();
-  ASSERT_TRUE(ss.str().compare("{\"a\":{\"b\":1}}") == 0);
-}
-
-TEST(JSStreamWriter, NestedObjectInArray) {
-  std::stringstream ss;
-  JSStreamWriter b(ss);
-  b.BeginArray();
-  b.BeginObject();
-  b.Name("a");
-  b.Value(1);
-  b.EndObject();
-  b.EndArray();
-  ASSERT_TRUE(ss.str().compare("[{\"a\":1}]") == 0);
-}
-
-TEST(JSStreamWriter, NestedArrayInObject) {
-  std::stringstream ss;
-  JSStreamWriter b(ss);
-  b.BeginObject();
-  b.Name("a");
-  b.BeginArray();
-  b.Value(1);
-  b.EndArray();
-  b.EndObject();
-  ASSERT_TRUE(ss.str().compare("{\"a\":[1]}") == 0);
-}
-
-TEST(JSStreamWriter, StingEscaping) {
-  std::stringstream ss;
-  JSStreamWriter b(ss);
-  b.Value("a\"a");
-  ASSERT_TRUE(ss.str().compare("\"a\\\"a\"") == 0);
-
-  std::stringstream ss2;
-  JSStreamWriter b2(ss2);
-  b2.Value("a\na");
-  ASSERT_TRUE(ss2.str().compare("\"a\\u000Aa\"") == 0);
-}
-
-TEST(JSStreamWriter, ArrayOfOjects) {
-  std::stringstream ss;
-  JSStreamWriter b(ss);
-  b.BeginArray();
-    b.BeginObject();
-    b.EndObject();
-
-    b.BeginObject();
-    b.EndObject();
-  b.EndArray();
-  ASSERT_TRUE(ss.str().compare("[{},{}]") == 0);
-}
-
-TEST(JSStreamWriter, Complex) {
-  std::stringstream ss;
-  JSStreamWriter b(ss);
-  b.BeginObject();
-    b.Name("a");
-      b.BeginArray();
-        b.Value(1);
-
-        b.BeginObject();
-        b.EndObject();
-
-        b.BeginObject();
-          b.Name("b");
-          b.Value("c");
-        b.EndObject();
-      b.EndArray();
-
-    b.Name("b");
-      b.BeginArray();
-        b.BeginArray();
-        b.EndArray();
-      b.EndArray();
-  b.EndObject();
-  ASSERT_TRUE(ss.str().compare("{\"a\":[1,{},{\"b\":\"c\"}],\"b\":[[]]}") == 0);
-}
-
-TEST(JSStreamWriter, Complex2) {
-  std::stringstream ss;
-  JSStreamWriter b(ss);
-  b.BeginObject();
-    b.Name("a");
-      b.BeginArray();
-        b.BeginObject();
-          b.Name("b");
-            b.Value("c");
-          b.Name("d");
-            b.BeginArray();
-              b.BeginObject();
-                b.Name("e");
-                  b.BeginArray();
-                    b.BeginObject();
-                      b.Name("f");
-                        b.Value("g");
-                    b.EndObject();
-                    b.BeginObject();
-                      b.Name("h");
-                        b.Value("i");
-                    b.EndObject();
-                  b.EndArray();
-              b.EndObject();
-            b.EndArray();
-        b.EndObject();
-      b.EndArray();
-  b.EndObject();
-  ASSERT_TRUE(ss.str().compare("{\"a\":[{\"b\":\"c\",\"d\":[{\"e\":[{\"f\":\"g\"},{\"h\":\"i\"}]}]}]}") == 0);
-}
--- a/tools/profiler/tests/gtest/moz.build
+++ b/tools/profiler/tests/gtest/moz.build
@@ -9,15 +9,14 @@ if CONFIG['OS_TARGET'] in ('Android', 'L
         'LulTest.cpp',
     ]
 
 LOCAL_INCLUDES += [
     '/tools/profiler',
 ]
 
 UNIFIED_SOURCES += [
-    'JSStreamWriterTest.cpp',
     'ThreadProfileTest.cpp',
 ]
 
 FINAL_LIBRARY = 'xul-gtest'
 
 FAIL_ON_WARNINGS = True