Bug 1154115 - Rewrite profiler JSON streaming. (r=mstange)
authorShu-yu Guo <shu@rfrn.org>
Mon, 11 May 2015 14:16:44 -0700
changeset 243412 ea1f7a05bd3205e41129bf4c1dd3f7e62248943f
parent 243411 cb4b66d730b3f517f786af925f36b84d78c3672d
child 243413 0049538641a2aac08175474834c2978b18c8a5f6
push id28738
push usercbook@mozilla.com
push dateTue, 12 May 2015 14:11:31 +0000
treeherdermozilla-central@bedce1b405a3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmstange
bugs1154115
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1154115 - Rewrite profiler JSON streaming. (r=mstange)
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