Bug 1429904 - When a JSContext for a thread is about to go away, collect enough information about any JIT entries in the buffer so that the entire buffer can be streamed to JSON. r?njn draft
authorMarkus Stange <mstange@themasta.com>
Wed, 28 Feb 2018 00:17:16 -0500
changeset 762110 3408bf0d74379d3554f353e4501e616930b100b0
parent 762109 c6b5aa2d7edd7d667d7a4563578ef7614a6d58e8
child 762111 891d9289bd4a2b92cdc750ee9c5304392171800a
push id101088
push userbmo:mstange@themasta.com
push dateThu, 01 Mar 2018 20:52:26 +0000
reviewersnjn
bugs1429904
milestone60.0a1
Bug 1429904 - When a JSContext for a thread is about to go away, collect enough information about any JIT entries in the buffer so that the entire buffer can be streamed to JSON. r?njn This changeset changes behavior. If the profile is streamed before any JSContext has gone away, we now iterate over the entire buffer twice (per thread): First, to collect information about JIT frames, and then again when we build the JSON for the samples. The first traversal stores small pieces of JSON for JIT fromes in individual strings, and the second iteration splices those strings into the thread JSON's frame table. When the JSContext for a thread goes away, we no longer build JSON for samples, and we don't reset the profiler buffer. We now only build the JSON for JIT frames. Once the complete profile is requested and we build samples for it, we iterate over the entire buffer, and look up the cached JIT frame information for JitReturnAddr entries from the correct range. Different parts of the buffer may correspond to the life time of different JSContexts: For each JSContext we will have one range in the JITFrameInfo, and we can look up the correct range based on the buffer position of the JitReturnAddr entry that we're processing. This new way of doing things has multiple advantages: - We no longer reset the buffer, so we no longer lose information about other threads. - All threads from a given process now always have sample data for the same time range. Before this change, the "partial profile" from a thread that lost its JSContext could extend further into the past than the other threads' profiles. - Requesting profiles multiple times now has more consistent results. Before this change, the first requested profile would include the partial profile, but then the partial profile was discarded. And the second requested profile would not contain any data for the time before the JSContext went away. - We now do less work when a thread's JSContext goes away. This should decrease the interruption time. MozReview-Commit-ID: 3KhnPtBijna
tools/profiler/core/ProfileBuffer.h
tools/profiler/core/ProfileBufferEntry.cpp
tools/profiler/core/ProfileBufferEntry.h
tools/profiler/core/ProfiledThreadData.cpp
tools/profiler/core/ProfiledThreadData.h
tools/profiler/core/ProfilerBacktrace.cpp
--- a/tools/profiler/core/ProfileBuffer.h
+++ b/tools/profiler/core/ProfileBuffer.h
@@ -52,19 +52,28 @@ public:
 
   // Add JIT frame information to aJITFrameInfo for any JitReturnAddr entries
   // that are currently in the buffer at or after aRangeStart, in samples
   // for the given thread.
   void AddJITInfoForRange(uint64_t aRangeStart,
                           int aThreadId, JSContext* aContext,
                           JITFrameInfo& aJITFrameInfo) const;
 
+  // Stream JSON for samples in the buffer to aWriter, using the supplied
+  // UniqueStacks object.
+  // Only streams samples for the given thread ID and which were taken at or
+  // after aSinceTime.
+  // aUniqueStacks needs to contain information about any JIT frames that we
+  // might encounter in the buffer, before this method is called. In other
+  // words, you need to have called AddJITInfoForRange for every range that
+  // might contain JIT frame information before calling this method.
   bool StreamSamplesToJSON(SpliceableJSONWriter& aWriter, int aThreadId,
                            double aSinceTime, JSContext* cx,
                            UniqueStacks& aUniqueStacks) const;
+
   bool StreamMarkersToJSON(SpliceableJSONWriter& aWriter, int aThreadId,
                            const mozilla::TimeStamp& aProcessStartTime,
                            double aSinceTime,
                            UniqueStacks& aUniqueStacks) const;
   void StreamPausedRangesToJSON(SpliceableJSONWriter& aWriter,
                                 double aSinceTime) const;
 
   // Find (via |aLastSample|) the most recent sample for the thread denoted by
--- a/tools/profiler/core/ProfileBufferEntry.cpp
+++ b/tools/profiler/core/ProfileBufferEntry.cpp
@@ -333,38 +333,30 @@ JITFrameInfoForBufferRange::Clone() cons
 JITFrameInfo::JITFrameInfo(const JITFrameInfo& aOther)
   : mUniqueStrings(MakeUnique<UniqueJSONStrings>(*aOther.mUniqueStrings))
 {
   for (const JITFrameInfoForBufferRange& range : aOther.mRanges) {
     mRanges.AppendElement(range.Clone());
   }
 }
 
-uint32_t
-UniqueStacks::JITAddress::Hash() const
-{
-  uint32_t hash = 0;
-  hash = AddToHash(hash, mAddress);
-  hash = AddToHash(hash, mStreamingGen);
-  return hash;
-}
-
 bool
 UniqueStacks::FrameKey::NormalFrameData::operator==(const NormalFrameData& aOther) const
 {
   return mLocation == aOther.mLocation &&
          mLine == aOther.mLine &&
          mCategory == aOther.mCategory;
 }
 
 bool
 UniqueStacks::FrameKey::JITFrameData::operator==(const JITFrameData& aOther) const
 {
-  return mAddress == aOther.mAddress &&
-         mDepth == aOther.mDepth;
+  return mCanonicalAddress == aOther.mCanonicalAddress &&
+         mDepth == aOther.mDepth &&
+         mRangeIndex == aOther.mRangeIndex;
 }
 
 uint32_t
 UniqueStacks::FrameKey::Hash() const
 {
   uint32_t hash = 0;
   if (mData.is<NormalFrameData>()) {
     const NormalFrameData& data = mData.as<NormalFrameData>();
@@ -374,24 +366,30 @@ UniqueStacks::FrameKey::Hash() const
     if (data.mLine.isSome()) {
       hash = AddToHash(hash, *data.mLine);
     }
     if (data.mCategory.isSome()) {
       hash = AddToHash(hash, *data.mCategory);
     }
   } else {
     const JITFrameData& data = mData.as<JITFrameData>();
-    hash = AddToHash(hash, data.mAddress.Hash());
+    hash = AddToHash(hash, data.mCanonicalAddress);
     hash = AddToHash(hash, data.mDepth);
+    hash = AddToHash(hash, data.mRangeIndex);
   }
   return hash;
 }
 
-UniqueStacks::UniqueStacks()
-  : mUniqueStrings(MakeUnique<UniqueJSONStrings>())
+// Consume aJITFrameInfo by stealing its string table and its JIT frame info
+// ranges. The JIT frame info contains JSON which refers to strings from the
+// JIT frame info's string table, so our string table needs to have the same
+// strings at the same indices.
+UniqueStacks::UniqueStacks(JITFrameInfo&& aJITFrameInfo)
+  : mUniqueStrings(Move(aJITFrameInfo.mUniqueStrings))
+  , mJITInfoRanges(Move(aJITFrameInfo.mRanges))
 {
   mFrameTableWriter.StartBareList();
   mStackTableWriter.StartBareList();
 }
 
 uint32_t UniqueStacks::GetOrAddStackIndex(const StackKey& aStack)
 {
   uint32_t index;
@@ -401,55 +399,67 @@ uint32_t UniqueStacks::GetOrAddStackInde
   }
 
   index = mStackToIndexMap.Count();
   mStackToIndexMap.Put(aStack, index);
   StreamStack(aStack);
   return index;
 }
 
-MOZ_MUST_USE nsTArray<UniqueStacks::FrameKey>
-UniqueStacks::GetOrAddJITFrameKeysForAddress(JSContext* aContext,
-                                             const JITAddress& aJITAddress)
+template<typename RangeT, typename PosT>
+struct PositionInRangeComparator final
 {
-  nsTArray<FrameKey>& frameKeys =
-    *mAddressToJITFrameKeysMap.LookupOrAdd(aJITAddress);
-
-  if (frameKeys.IsEmpty()) {
-    for (JS::ProfiledFrameHandle handle :
-           JS::GetProfiledFrames(aContext, aJITAddress.mAddress)) {
-      // JIT frames with the same canonical address should be treated as the
-      // same frame, so set the frame key's address to the canonical address.
-      FrameKey frameKey(
-        JITAddress{ handle.canonicalAddress(), aJITAddress.mStreamingGen },
-        frameKeys.Length());
-      MaybeAddJITFrameIndex(aContext, frameKey, handle);
-      frameKeys.AppendElement(frameKey);
-    }
-    MOZ_ASSERT(frameKeys.Length() > 0);
+  bool Equals(const RangeT& aRange, PosT aPos) const
+  {
+    return aRange.mRangeStart <= aPos && aPos < aRange.mRangeEnd;
   }
 
-  // Return a copy of the array.
-  return nsTArray<FrameKey>(frameKeys);
-}
+  bool LessThan(const RangeT& aRange, PosT aPos) const
+  {
+    return aRange.mRangeEnd <= aPos;
+  }
+};
 
-void
-UniqueStacks::MaybeAddJITFrameIndex(JSContext* aContext,
-                                    const FrameKey& aFrame,
-                                    const JS::ProfiledFrameHandle& aJITFrame)
+Maybe<nsTArray<UniqueStacks::FrameKey>>
+UniqueStacks::LookupFramesForJITAddressFromBufferPos(void* aJITAddress,
+                                                     uint64_t aBufferPos)
 {
-  uint32_t index;
-  if (mFrameToIndexMap.Get(aFrame, &index)) {
-    MOZ_ASSERT(index < mFrameToIndexMap.Count());
-    return;
+  size_t rangeIndex = mJITInfoRanges.BinaryIndexOf(aBufferPos,
+    PositionInRangeComparator<JITFrameInfoForBufferRange, uint64_t>());
+  MOZ_RELEASE_ASSERT(rangeIndex != mJITInfoRanges.NoIndex,
+                     "Buffer position of jit address needs to be in one of the ranges");
+
+  using JITFrameKey = JITFrameInfoForBufferRange::JITFrameKey;
+
+  const JITFrameInfoForBufferRange& jitFrameInfoRange = mJITInfoRanges[rangeIndex];
+  const nsTArray<JITFrameKey>* jitFrameKeys =
+    jitFrameInfoRange.mJITAddressToJITFramesMap.Get(aJITAddress);
+  if (!jitFrameKeys) {
+    return Nothing();
   }
 
-  index = mFrameToIndexMap.Count();
-  mFrameToIndexMap.Put(aFrame, index);
-  StreamJITFrame(aContext, aJITFrame);
+  // Map the array of JITFrameKeys to an array of FrameKeys, and ensure that
+  // each of the FrameKeys exists in mFrameToIndexMap.
+  nsTArray<FrameKey> frameKeys;
+  for (const JITFrameKey& jitFrameKey : *jitFrameKeys) {
+    FrameKey frameKey(jitFrameKey.mCanonicalAddress, jitFrameKey.mDepth, rangeIndex);
+    if (!mFrameToIndexMap.Contains(frameKey)) {
+      // We need to add this frame to our frame table. The JSON for this frame
+      // already exists in jitFrameInfoRange, we just need to splice it into
+      // the frame table and give it an index.
+      uint32_t index = mFrameToIndexMap.Count();
+      const nsCString* frameJSON =
+        jitFrameInfoRange.mJITFrameToFrameJSONMap.Get(jitFrameKey);
+      MOZ_RELEASE_ASSERT(frameJSON, "Should have cached JSON for this frame");
+      mFrameTableWriter.Splice(frameJSON->get());
+      mFrameToIndexMap.Put(frameKey, index);
+    }
+    frameKeys.AppendElement(Move(frameKey));
+  }
+  return Some(Move(frameKeys));
 }
 
 uint32_t
 UniqueStacks::GetOrAddFrameIndex(const FrameKey& aFrame)
 {
   uint32_t index;
   if (mFrameToIndexMap.Get(aFrame, &index)) {
     MOZ_ASSERT(index < mFrameToIndexMap.Count());
@@ -701,24 +711,16 @@ JITFrameInfo::AddInfoForRange(uint64_t a
   });
 
   mRanges.AppendElement(JITFrameInfoForBufferRange{
     aRangeStart, aRangeEnd,
     Move(jitAddressToJITFrameMap), Move(jitFrameToFrameJSONMap)
   });
 }
 
-// This method will go away in the next patch.
-void
-UniqueStacks::StreamJITFrame(JSContext* aContext,
-                             const JS::ProfiledFrameHandle& aJITFrame)
-{
-  ::StreamJITFrame(aContext, mFrameTableWriter, *mUniqueStrings, aJITFrame);
-}
-
 struct ProfileSample
 {
   uint32_t mStack;
   double mTime;
   Maybe<double> mResponsiveness;
   Maybe<double> mRSS;
   Maybe<double> mUSS;
 };
@@ -767,16 +769,17 @@ public:
                          aInitialReadPos <= aBuffer.mRangeEnd);
       mReadPos = aInitialReadPos;
     }
   }
 
   bool Has() const { return mReadPos != mBuffer.mRangeEnd; }
   const ProfileBufferEntry& Get() const { return mBuffer.GetEntry(mReadPos); }
   void Next() { mReadPos++; }
+  uint64_t CurPos() { return mReadPos; }
 
 private:
   const ProfileBuffer& mBuffer;
   uint64_t mReadPos;
 };
 
 // The following grammar shows legal sequences of profile buffer entries.
 // The sequences beginning with a ThreadId entry are known as "samples".
@@ -1027,25 +1030,23 @@ ProfileBuffer::StreamSamplesToJSON(Splic
         }
 
         stack = aUniqueStacks.AppendFrame(
           stack, UniqueStacks::FrameKey(strbuf.get(), line, category));
 
       } else if (e.Get().IsJitReturnAddr()) {
         numFrames++;
 
-        // We can only process JitReturnAddr entries if we have a JSContext.
-        MOZ_RELEASE_ASSERT(aContext);
-
         // A JIT frame may expand to multiple frames due to inlining.
         void* pc = e.Get().u.mPtr;
-        UniqueStacks::JITAddress address = { pc, aUniqueStacks.CurrentGen() };
-        nsTArray<UniqueStacks::FrameKey> frameKeys =
-          aUniqueStacks.GetOrAddJITFrameKeysForAddress(aContext, address);
-        for (const UniqueStacks::FrameKey& frameKey : frameKeys) {
+        const Maybe<nsTArray<UniqueStacks::FrameKey>>& frameKeys =
+          aUniqueStacks.LookupFramesForJITAddressFromBufferPos(pc, e.CurPos());
+        MOZ_RELEASE_ASSERT(frameKeys,
+          "Attempting to stream samples for a buffer range for which we don't have JITFrameInfo?");
+        for (const UniqueStacks::FrameKey& frameKey : *frameKeys) {
           stack = aUniqueStacks.AppendFrame(stack, frameKey);
         }
 
         e.Next();
 
       } else {
         break;
       }
--- a/tools/profiler/core/ProfileBufferEntry.h
+++ b/tools/profiler/core/ProfileBufferEntry.h
@@ -211,55 +211,31 @@ struct JITFrameInfo final
   // The string table which contains strings used in the frame JSON that's
   // cached in mRanges.
   mozilla::UniquePtr<UniqueJSONStrings> mUniqueStrings;
 };
 
 class UniqueStacks
 {
 public:
-  // We de-duplicate information about JIT frames based on the return address
-  // of the frame. However, if the same UniqueStacks object is used to stream
-  // profiler buffer contents more than once, then in the time between the two
-  // stream attempts ("streaming generations"), JIT code can have been freed
-  // and reallocated in the same areas of memory. Consequently, during the next
-  // streaming generation, we may see JIT return addresses that we've already
-  // seen before, but which now represent completely different JS functions.
-  // So we need to make sure that any de-duplication takes the streaming
-  // generation into account and does not only compare the address itself.
-  // The JITAddress struct packages the two together and can be used as a hash
-  // key.
-  struct JITAddress
-  {
-    void* mAddress;
-    uint32_t mStreamingGen;
-
-    uint32_t Hash() const;
-    bool operator==(const JITAddress& aRhs) const
-    {
-      return mAddress == aRhs.mAddress && mStreamingGen == aRhs.mStreamingGen;
-    }
-    bool operator!=(const JITAddress& aRhs) const { return !(*this == aRhs); }
-  };
-
   struct FrameKey {
     explicit FrameKey(const char* aLocation)
       : mData(NormalFrameData{
                 nsCString(aLocation), mozilla::Nothing(), mozilla::Nothing() })
     {
     }
 
     FrameKey(const char* aLocation, const mozilla::Maybe<unsigned>& aLine,
              const mozilla::Maybe<unsigned>& aCategory)
       : mData(NormalFrameData{ nsCString(aLocation), aLine, aCategory })
     {
     }
 
-    FrameKey(const JITAddress& aJITAddress, uint32_t aJITDepth)
-      : mData(JITFrameData{ aJITAddress, aJITDepth })
+    FrameKey(void* aJITAddress, uint32_t aJITDepth, uint32_t aRangeIndex)
+      : mData(JITFrameData{ aJITAddress, aJITDepth, aRangeIndex })
     {
     }
 
     FrameKey(const FrameKey& aToCopy) = default;
 
     uint32_t Hash() const;
     bool operator==(const FrameKey& aOther) const { return mData == aOther.mData; }
 
@@ -268,18 +244,19 @@ public:
 
       nsCString mLocation;
       mozilla::Maybe<unsigned> mLine;
       mozilla::Maybe<unsigned> mCategory;
     };
     struct JITFrameData {
       bool operator==(const JITFrameData& aOther) const;
 
-      JITAddress mAddress;
+      void* mCanonicalAddress;
       uint32_t mDepth;
+      uint32_t mRangeIndex;
     };
     mozilla::Variant<NormalFrameData, JITFrameData> mData;
   };
 
   struct StackKey {
     mozilla::Maybe<uint32_t> mPrefixStackIndex;
     uint32_t mFrameIndex;
 
@@ -301,71 +278,55 @@ public:
       return mPrefixStackIndex == aOther.mPrefixStackIndex &&
              mFrameIndex == aOther.mFrameIndex;
     }
 
   private:
     uint32_t mHash;
   };
 
-  explicit UniqueStacks();
-
-  // Needs to be called when using a UniqueStacks object again after having
-  // streamed entries that are no longer in the buffer (and which could have
-  // been GC'ed and their memory reused).
-  void AdvanceStreamingGeneration() { mStreamingGeneration++; }
-  uint32_t CurrentGen() { return mStreamingGeneration; }
+  explicit UniqueStacks(JITFrameInfo&& aJITFrameInfo);
 
   // Return a StackKey for aFrame as the stack's root frame (no prefix).
   MOZ_MUST_USE StackKey BeginStack(const FrameKey& aFrame);
 
   // Return a new StackKey that is obtained by appending aFrame to aStack.
   MOZ_MUST_USE StackKey AppendFrame(const StackKey& aStack,
                                     const FrameKey& aFrame);
 
-  MOZ_MUST_USE nsTArray<FrameKey>
-  GetOrAddJITFrameKeysForAddress(JSContext* aContext,
-                                 const JITAddress& aJITAddress);
+  // Look up frame keys for the given JIT address, and ensure that our frame
+  // table has entries for the returned frame keys. The JSON for these frames
+  // is taken from mJITInfoRanges.
+  // aBufferPosition is needed in order to look up the correct JIT frame info
+  // object in mJITInfoRanges.
+  MOZ_MUST_USE mozilla::Maybe<nsTArray<UniqueStacks::FrameKey>>
+  LookupFramesForJITAddressFromBufferPos(void* aJITAddress,
+                                         uint64_t aBufferPosition);
 
   MOZ_MUST_USE uint32_t GetOrAddFrameIndex(const FrameKey& aFrame);
   MOZ_MUST_USE uint32_t GetOrAddStackIndex(const StackKey& aStack);
 
   void SpliceFrameTableElements(SpliceableJSONWriter& aWriter);
   void SpliceStackTableElements(SpliceableJSONWriter& aWriter);
 
 private:
-  // Make sure that there exists a frame index for aFrame, and if there isn't
-  // one already, create one and call StreamJITFrame for the frame.
-  void MaybeAddJITFrameIndex(JSContext* aContext,
-                             const FrameKey& aFrame,
-                             const JS::ProfiledFrameHandle& aJITFrame);
-
   void StreamNonJITFrame(const FrameKey& aFrame);
-  void StreamJITFrame(JSContext* aContext,
-                      const JS::ProfiledFrameHandle& aJITFrame);
   void StreamStack(const StackKey& aStack);
 
 public:
   mozilla::UniquePtr<UniqueJSONStrings> mUniqueStrings;
 
 private:
-  // To avoid incurring JitcodeGlobalTable lookup costs for every JIT frame,
-  // we cache the frame keys of frames keyed by JIT code address. All FrameKeys
-  // in mAddressToJITFrameKeysMap are guaranteed to be in mFrameToIndexMap.
-  nsClassHashtable<nsGenericHashKey<JITAddress>, nsTArray<FrameKey>> mAddressToJITFrameKeysMap;
-
   SpliceableChunkedJSONWriter mFrameTableWriter;
   nsDataHashtable<nsGenericHashKey<FrameKey>, uint32_t> mFrameToIndexMap;
 
   SpliceableChunkedJSONWriter mStackTableWriter;
   nsDataHashtable<nsGenericHashKey<StackKey>, uint32_t> mStackToIndexMap;
 
-  // Used to avoid collisions between JITAddresses that refer to different
-  // frames.
-  uint32_t mStreamingGeneration;
+  nsTArray<JITFrameInfoForBufferRange> mJITInfoRanges;
 };
 
 //
 // Thread profile JSON Format
 // --------------------------
 //
 // The profile contains much duplicate information. The output JSON of the
 // profile attempts to deduplicate strings, frames, and stack prefixes, to cut
--- a/tools/profiler/core/ProfiledThreadData.cpp
+++ b/tools/profiler/core/ProfiledThreadData.cpp
@@ -30,53 +30,55 @@ ProfiledThreadData::~ProfiledThreadData(
   MOZ_COUNT_DTOR(ProfiledThreadData);
 }
 
 void
 ProfiledThreadData::StreamJSON(const ProfileBuffer& aBuffer, JSContext* aCx,
                                SpliceableJSONWriter& aWriter,
                                const TimeStamp& aProcessStartTime, double aSinceTime)
 {
-  UniquePtr<PartialThreadProfile> partialProfile = Move(mPartialProfile);
-
-  UniquePtr<UniqueStacks> uniqueStacks = partialProfile
-    ? Move(partialProfile->mUniqueStacks)
-    : MakeUnique<UniqueStacks>();
+  if (mJITFrameInfoForPreviousJSContexts &&
+      mJITFrameInfoForPreviousJSContexts->HasExpired(aBuffer.mRangeStart)) {
+    mJITFrameInfoForPreviousJSContexts = nullptr;
+  }
 
-  uniqueStacks->AdvanceStreamingGeneration();
+  // If we have an existing JITFrameInfo in mJITFrameInfoForPreviousJSContexts,
+  // copy the data from it.
+  JITFrameInfo jitFrameInfo = mJITFrameInfoForPreviousJSContexts
+    ? JITFrameInfo(*mJITFrameInfoForPreviousJSContexts) : JITFrameInfo();
 
-  UniquePtr<char[]> partialSamplesJSON;
-  UniquePtr<char[]> partialMarkersJSON;
-  if (partialProfile) {
-    partialSamplesJSON = Move(partialProfile->mSamplesJSON);
-    partialMarkersJSON = Move(partialProfile->mMarkersJSON);
+  if (aCx && mBufferPositionWhenReceivedJSContext) {
+    aBuffer.AddJITInfoForRange(*mBufferPositionWhenReceivedJSContext,
+      mThreadInfo->ThreadId(), aCx, jitFrameInfo);
   }
 
+  UniqueStacks uniqueStacks(Move(jitFrameInfo));
+
   aWriter.Start();
   {
     StreamSamplesAndMarkers(mThreadInfo->Name(), mThreadInfo->ThreadId(),
                             aBuffer, aWriter,
                             aProcessStartTime,
                             mThreadInfo->RegisterTime(), mUnregisterTime,
                             aSinceTime, aCx,
-                            Move(partialSamplesJSON),
-                            Move(partialMarkersJSON),
-                            *uniqueStacks);
+                            nullptr,
+                            nullptr,
+                            uniqueStacks);
 
     aWriter.StartObjectProperty("stackTable");
     {
       {
         JSONSchemaWriter schema(aWriter);
         schema.WriteField("prefix");
         schema.WriteField("frame");
       }
 
       aWriter.StartArrayProperty("data");
       {
-        uniqueStacks->SpliceStackTableElements(aWriter);
+        uniqueStacks.SpliceStackTableElements(aWriter);
       }
       aWriter.EndArray();
     }
     aWriter.EndObject();
 
     aWriter.StartObjectProperty("frameTable");
     {
       {
@@ -85,25 +87,25 @@ ProfiledThreadData::StreamJSON(const Pro
         schema.WriteField("implementation");
         schema.WriteField("optimizations");
         schema.WriteField("line");
         schema.WriteField("category");
       }
 
       aWriter.StartArrayProperty("data");
       {
-        uniqueStacks->SpliceFrameTableElements(aWriter);
+        uniqueStacks.SpliceFrameTableElements(aWriter);
       }
       aWriter.EndArray();
     }
     aWriter.EndObject();
 
     aWriter.StartArrayProperty("stringTable");
     {
-      uniqueStacks->mUniqueStrings->SpliceStringTableElements(aWriter);
+      uniqueStacks.mUniqueStrings->SpliceStringTableElements(aWriter);
     }
     aWriter.EndArray();
   }
 
   aWriter.End();
 }
 
 void
@@ -187,101 +189,32 @@ StreamSamplesAndMarkers(const char* aNam
                                   aSinceTime, aUniqueStacks);
     }
     aWriter.EndArray();
   }
   aWriter.EndObject();
 }
 
 void
-ProfiledThreadData::NotifyAboutToLoseJSContext(JSContext* aCx,
+ProfiledThreadData::NotifyAboutToLoseJSContext(JSContext* aContext,
                                                const TimeStamp& aProcessStartTime,
                                                ProfileBuffer& aBuffer)
 {
-  // This function is used to serialize the current buffer just before
-  // JSContext destruction.
-  MOZ_ASSERT(aCx);
-
-  // Unlike StreamJSObject, do not surround the samples in brackets by calling
-  // 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.
-  UniquePtr<UniqueStacks> uniqueStacks = mPartialProfile
-    ? Move(mPartialProfile->mUniqueStacks)
-    : MakeUnique<UniqueStacks>();
-
-  uniqueStacks->AdvanceStreamingGeneration();
-
-  UniquePtr<char[]> samplesJSON;
-  UniquePtr<char[]> markersJSON;
-
-  {
-    SpliceableChunkedJSONWriter b;
-    b.StartBareList();
-    bool haveSamples = false;
-    {
-      if (mPartialProfile && mPartialProfile->mSamplesJSON) {
-        b.Splice(mPartialProfile->mSamplesJSON.get());
-        haveSamples = true;
-      }
-
-      // We deliberately use a new variable instead of writing something like
-      // `haveSamples || aBuffer.StreamSamplesToJSON(...)` because we don't want
-      // to short-circuit the call.
-      bool streamedNewSamples =
-        aBuffer.StreamSamplesToJSON(b, mThreadInfo->ThreadId(),
-                                    /* aSinceTime = */ 0,
-                                    aCx, *uniqueStacks);
-      haveSamples = haveSamples || streamedNewSamples;
-    }
-    b.EndBareList();
-
-    // https://bugzilla.mozilla.org/show_bug.cgi?id=1428076
-    // If we don't have any data, keep samplesJSON set to null. That
-    // way we won't try to splice it into the JSON later on, which would
-    // result in an invalid JSON due to stray commas.
-    if (haveSamples) {
-      samplesJSON = b.WriteFunc()->CopyData();
-    }
+  if (!mBufferPositionWhenReceivedJSContext) {
+    return;
   }
 
-  {
-    SpliceableChunkedJSONWriter b;
-    b.StartBareList();
-    bool haveMarkers = false;
-    {
-      if (mPartialProfile && mPartialProfile->mMarkersJSON) {
-        b.Splice(mPartialProfile->mMarkersJSON.get());
-        haveMarkers = true;
-      }
+  MOZ_RELEASE_ASSERT(aContext);
 
-      // We deliberately use a new variable instead of writing something like
-      // `haveMarkers || aBuffer.StreamMarkersToJSON(...)` because we don't want
-      // to short-circuit the call.
-      bool streamedNewMarkers =
-        aBuffer.StreamMarkersToJSON(b, mThreadInfo->ThreadId(),
-                                    aProcessStartTime,
-                                    /* aSinceTime = */ 0, *uniqueStacks);
-      haveMarkers = haveMarkers || streamedNewMarkers;
-    }
-    b.EndBareList();
-
-    // https://bugzilla.mozilla.org/show_bug.cgi?id=1428076
-    // If we don't have any data, keep markersJSON set to null. That
-    // way we won't try to splice it into the JSON later on, which would
-    // result in an invalid JSON due to stray commas.
-    if (haveMarkers) {
-      markersJSON = b.WriteFunc()->CopyData();
-    }
+  if (mJITFrameInfoForPreviousJSContexts &&
+      mJITFrameInfoForPreviousJSContexts->HasExpired(aBuffer.mRangeStart)) {
+    mJITFrameInfoForPreviousJSContexts = nullptr;
   }
 
-  mPartialProfile = MakeUnique<PartialThreadProfile>(
-    Move(samplesJSON), Move(markersJSON), Move(uniqueStacks));
-
-  mBufferPositionWhenReceivedJSContext = Nothing();
+  UniquePtr<JITFrameInfo> jitFrameInfo = mJITFrameInfoForPreviousJSContexts
+    ? Move(mJITFrameInfoForPreviousJSContexts) : MakeUnique<JITFrameInfo>();
 
-  // Reset the buffer. Attempting to symbolicate JS samples after mContext has
-  // gone away will crash.
-  aBuffer.Reset();
+  aBuffer.AddJITInfoForRange(*mBufferPositionWhenReceivedJSContext,
+     mThreadInfo->ThreadId(), aContext, *jitFrameInfo);
+
+  mJITFrameInfoForPreviousJSContexts = Move(jitFrameInfo);
+  mBufferPositionWhenReceivedJSContext = Nothing();
 }
--- a/tools/profiler/core/ProfiledThreadData.h
+++ b/tools/profiler/core/ProfiledThreadData.h
@@ -11,33 +11,16 @@
 #include "mozilla/TimeStamp.h"
 #include "mozilla/UniquePtrExtensions.h"
 
 #include "js/ProfilingStack.h"
 #include "platform.h"
 #include "ProfileBuffer.h"
 #include "ThreadInfo.h"
 
-// Contains data for partial profiles that get saved when
-// ThreadInfo::FlushSamplesAndMarkers gets called.
-struct PartialThreadProfile final
-{
-  PartialThreadProfile(mozilla::UniquePtr<char[]>&& aSamplesJSON,
-                       mozilla::UniquePtr<char[]>&& aMarkersJSON,
-                       mozilla::UniquePtr<UniqueStacks>&& aUniqueStacks)
-    : mSamplesJSON(mozilla::Move(aSamplesJSON))
-    , mMarkersJSON(mozilla::Move(aMarkersJSON))
-    , mUniqueStacks(mozilla::Move(aUniqueStacks))
-  {}
-
-  mozilla::UniquePtr<char[]> mSamplesJSON;
-  mozilla::UniquePtr<char[]> mMarkersJSON;
-  mozilla::UniquePtr<UniqueStacks> mUniqueStacks;
-};
-
 // This class contains information about a thread that is only relevant while
 // the profiler is running, for any threads (both alive and dead) whose thread
 // name matches the "thread filter" in the current profiler run.
 // ProfiledThreadData objects may be kept alive even after the thread is
 // unregistered, as long as there is still data for that thread in the profiler
 // buffer.
 //
 // Accesses to this class are protected by the profiler state lock.
@@ -104,21 +87,21 @@ public:
 private:
   // Group A:
   // The following fields are interesting for the entire lifetime of a
   // ProfiledThreadData object.
 
   // This thread's thread info.
   const RefPtr<ThreadInfo> mThreadInfo;
 
-  // 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.
-  UniquePtr<PartialThreadProfile> mPartialProfile;
+  // Contains JSON for JIT frames from any JSContexts that were used for this
+  // thread in the past.
+  // Null if this thread has never lost a JSContext or if all samples from
+  // previous JSContexts have been evicted from the profiler buffer.
+  UniquePtr<JITFrameInfo> mJITFrameInfoForPreviousJSContexts;
 
   // Group B:
   // The following fields are only used while this thread is alive and
   // registered. They become Nothing() once the thread is unregistered.
 
   // A helper object that instruments nsIThreads to obtain responsiveness
   // information about their event loop.
   mozilla::Maybe<ThreadResponsiveness> mResponsiveness;
--- a/tools/profiler/core/ProfilerBacktrace.cpp
+++ b/tools/profiler/core/ProfilerBacktrace.cpp
@@ -23,20 +23,20 @@ ProfilerBacktrace::~ProfilerBacktrace()
   MOZ_COUNT_DTOR(ProfilerBacktrace);
 }
 
 void
 ProfilerBacktrace::StreamJSON(SpliceableJSONWriter& aWriter,
                               const TimeStamp& aProcessStartTime,
                               UniqueStacks& aUniqueStacks)
 {
-  // This call to StreamSamplesAndMarkers() can safely pass in a non-null
-  // JSContext. That's because StreamSamplesAndMarkers() only accesses the
-  // JSContext when streaming JitReturnAddress entries, and such entries
-  // never appear in synchronous samples.
+  // Unlike ProfiledThreadData::StreamJSON, we don't need to call
+  // ProfileBuffer::AddJITInfoForRange because mBuffer does not contain any
+  // JitReturnAddr entries. For synchronous samples, JIT frames get expanded
+  // at sample time.
   StreamSamplesAndMarkers(mName.get(), mThreadId,
                           *mBuffer.get(), aWriter, aProcessStartTime,
                           /* aRegisterTime */ TimeStamp(),
                           /* aUnregisterTime */ TimeStamp(),
                           /* aSinceTime */ 0,
                           /* aContext */ nullptr,
                           /* aSavedStreamedSamples */ nullptr,
                           /* aSavedStreamedMarkers */ nullptr,