mozglue/baseprofiler/core/ProfileBufferEntry.cpp
author sotaro <sotaro.ikeda.g@gmail.com>
Sat, 27 Nov 2021 02:24:22 +0000
changeset 600316 c4b3480996ec9bdbea7040e5cb9e05215d9acb16
parent 596527 e34e1bf0af098c4c46dccb0d69d8f1d14c6f5399
permissions -rw-r--r--
Bug 1743057 - Notify windows visibility change to WindowOcclusionTracker when window is activated r=gfx-reviewers,jrmuizel When Firefox was activated by Alt+Tabbing, there was a case that event hook of WindowOcclusionCalculator could not detect an event of window activated. Then it is necessary to trigger window occlusion calculation from nsWindows. Differential Revision: https://phabricator.services.mozilla.com/D132206

/* -*- 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 "ProfileBufferEntry.h"

#include <ostream>
#include <type_traits>

#include "mozilla/Logging.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Sprintf.h"
#include "mozilla/StackWalk.h"

#include "BaseProfiler.h"
#include "mozilla/BaseProfilerMarkers.h"
#include "platform.h"
#include "ProfileBuffer.h"
#include "ProfilerBacktrace.h"

namespace mozilla {
namespace baseprofiler {

////////////////////////////////////////////////////////////////////////
// BEGIN ProfileBufferEntry

ProfileBufferEntry::ProfileBufferEntry()
    : mKind(Kind::INVALID), mStorage{0, 0, 0, 0, 0, 0, 0, 0} {}

// aString must be a static string.
ProfileBufferEntry::ProfileBufferEntry(Kind aKind, const char* aString)
    : mKind(aKind) {
  memcpy(mStorage, &aString, sizeof(aString));
}

ProfileBufferEntry::ProfileBufferEntry(Kind aKind, char aChars[kNumChars])
    : mKind(aKind) {
  memcpy(mStorage, aChars, kNumChars);
}

ProfileBufferEntry::ProfileBufferEntry(Kind aKind, void* aPtr) : mKind(aKind) {
  memcpy(mStorage, &aPtr, sizeof(aPtr));
}

ProfileBufferEntry::ProfileBufferEntry(Kind aKind, double aDouble)
    : mKind(aKind) {
  memcpy(mStorage, &aDouble, sizeof(aDouble));
}

ProfileBufferEntry::ProfileBufferEntry(Kind aKind, int aInt) : mKind(aKind) {
  memcpy(mStorage, &aInt, sizeof(aInt));
}

ProfileBufferEntry::ProfileBufferEntry(Kind aKind, int64_t aInt64)
    : mKind(aKind) {
  memcpy(mStorage, &aInt64, sizeof(aInt64));
}

ProfileBufferEntry::ProfileBufferEntry(Kind aKind, uint64_t aUint64)
    : mKind(aKind) {
  memcpy(mStorage, &aUint64, sizeof(aUint64));
}

ProfileBufferEntry::ProfileBufferEntry(Kind aKind,
                                       BaseProfilerThreadId aThreadId)
    : mKind(aKind) {
  static_assert(std::is_trivially_copyable_v<BaseProfilerThreadId>);
  static_assert(sizeof(aThreadId) <= sizeof(mStorage));
  memcpy(mStorage, &aThreadId, sizeof(aThreadId));
}

const char* ProfileBufferEntry::GetString() const {
  const char* result;
  memcpy(&result, mStorage, sizeof(result));
  return result;
}

void* ProfileBufferEntry::GetPtr() const {
  void* result;
  memcpy(&result, mStorage, sizeof(result));
  return result;
}

double ProfileBufferEntry::GetDouble() const {
  double result;
  memcpy(&result, mStorage, sizeof(result));
  return result;
}

int ProfileBufferEntry::GetInt() const {
  int result;
  memcpy(&result, mStorage, sizeof(result));
  return result;
}

int64_t ProfileBufferEntry::GetInt64() const {
  int64_t result;
  memcpy(&result, mStorage, sizeof(result));
  return result;
}

uint64_t ProfileBufferEntry::GetUint64() const {
  uint64_t result;
  memcpy(&result, mStorage, sizeof(result));
  return result;
}

BaseProfilerThreadId ProfileBufferEntry::GetThreadId() const {
  BaseProfilerThreadId result;
  static_assert(std::is_trivially_copyable_v<BaseProfilerThreadId>);
  memcpy(&result, mStorage, sizeof(result));
  return result;
}

void ProfileBufferEntry::CopyCharsInto(char (&aOutArray)[kNumChars]) const {
  memcpy(aOutArray, mStorage, kNumChars);
}

// END ProfileBufferEntry
////////////////////////////////////////////////////////////////////////

// As mentioned in ProfileBufferEntry.h, the JSON format contains many
// arrays whose elements are laid out according to various schemas to help
// de-duplication. This RAII class helps write these arrays by keeping track of
// the last non-null element written and adding the appropriate number of null
// elements when writing new non-null elements. It also automatically opens and
// closes an array element on the given JSON writer.
//
// You grant the AutoArraySchemaWriter exclusive access to the JSONWriter and
// the UniqueJSONStrings objects for the lifetime of AutoArraySchemaWriter. Do
// not access them independently while the AutoArraySchemaWriter is alive.
// If you need to add complex objects, call FreeFormElement(), which will give
// you temporary access to the writer.
//
// Example usage:
//
//     // Define the schema of elements in this type of array: [FOO, BAR, BAZ]
//     enum Schema : uint32_t {
//       FOO = 0,
//       BAR = 1,
//       BAZ = 2
//     };
//
//     AutoArraySchemaWriter writer(someJsonWriter, someUniqueStrings);
//     if (shouldWriteFoo) {
//       writer.IntElement(FOO, getFoo());
//     }
//     ... etc ...
//
//     The elements need to be added in-order.
class MOZ_RAII AutoArraySchemaWriter {
 public:
  explicit AutoArraySchemaWriter(SpliceableJSONWriter& aWriter)
      : mJSONWriter(aWriter), mNextFreeIndex(0) {
    mJSONWriter.StartArrayElement(SpliceableJSONWriter::SingleLineStyle);
  }

  ~AutoArraySchemaWriter() { mJSONWriter.EndArray(); }

  template <typename T>
  void IntElement(uint32_t aIndex, T aValue) {
    static_assert(!std::is_same_v<T, uint64_t>,
                  "Narrowing uint64 -> int64 conversion not allowed");
    FillUpTo(aIndex);
    mJSONWriter.IntElement(static_cast<int64_t>(aValue));
  }

  void DoubleElement(uint32_t aIndex, double aValue) {
    FillUpTo(aIndex);
    mJSONWriter.DoubleElement(aValue);
  }

  void TimeMsElement(uint32_t aIndex, double aTime_ms) {
    FillUpTo(aIndex);
    mJSONWriter.TimeDoubleMsElement(aTime_ms);
  }

  void BoolElement(uint32_t aIndex, bool aValue) {
    FillUpTo(aIndex);
    mJSONWriter.BoolElement(aValue);
  }

 protected:
  SpliceableJSONWriter& Writer() { return mJSONWriter; }

  void FillUpTo(uint32_t aIndex) {
    MOZ_ASSERT(aIndex >= mNextFreeIndex);
    mJSONWriter.NullElements(aIndex - mNextFreeIndex);
    mNextFreeIndex = aIndex + 1;
  }

 private:
  SpliceableJSONWriter& mJSONWriter;
  uint32_t mNextFreeIndex;
};

// Same as AutoArraySchemaWriter, but this can also write strings (output as
// indexes into the table of unique strings).
class MOZ_RAII AutoArraySchemaWithStringsWriter : public AutoArraySchemaWriter {
 public:
  AutoArraySchemaWithStringsWriter(SpliceableJSONWriter& aWriter,
                                   UniqueJSONStrings& aStrings)
      : AutoArraySchemaWriter(aWriter), mStrings(aStrings) {}

  void StringElement(uint32_t aIndex, const Span<const char>& aValue) {
    FillUpTo(aIndex);
    mStrings.WriteElement(Writer(), aValue);
  }

 private:
  UniqueJSONStrings& mStrings;
};

UniqueStacks::StackKey UniqueStacks::BeginStack(const FrameKey& aFrame) {
  return StackKey(GetOrAddFrameIndex(aFrame));
}

UniqueStacks::StackKey UniqueStacks::AppendFrame(const StackKey& aStack,
                                                 const FrameKey& aFrame) {
  return StackKey(aStack, GetOrAddStackIndex(aStack),
                  GetOrAddFrameIndex(aFrame));
}

bool UniqueStacks::FrameKey::NormalFrameData::operator==(
    const NormalFrameData& aOther) const {
  return mLocation == aOther.mLocation &&
         mRelevantForJS == aOther.mRelevantForJS &&
         mInnerWindowID == aOther.mInnerWindowID && mLine == aOther.mLine &&
         mColumn == aOther.mColumn && mCategoryPair == aOther.mCategoryPair;
}

UniqueStacks::UniqueStacks() : mUniqueStrings(MakeUnique<UniqueJSONStrings>()) {
  mFrameTableWriter.StartBareList();
  mStackTableWriter.StartBareList();
}

uint32_t UniqueStacks::GetOrAddStackIndex(const StackKey& aStack) {
  uint32_t count = mStackToIndexMap.count();
  auto entry = mStackToIndexMap.lookupForAdd(aStack);
  if (entry) {
    MOZ_ASSERT(entry->value() < count);
    return entry->value();
  }

  MOZ_RELEASE_ASSERT(mStackToIndexMap.add(entry, aStack, count));
  StreamStack(aStack);
  return count;
}

uint32_t UniqueStacks::GetOrAddFrameIndex(const FrameKey& aFrame) {
  uint32_t count = mFrameToIndexMap.count();
  auto entry = mFrameToIndexMap.lookupForAdd(aFrame);
  if (entry) {
    MOZ_ASSERT(entry->value() < count);
    return entry->value();
  }

  MOZ_RELEASE_ASSERT(mFrameToIndexMap.add(entry, aFrame, count));
  StreamNonJITFrame(aFrame);
  return count;
}

void UniqueStacks::SpliceFrameTableElements(SpliceableJSONWriter& aWriter) {
  mFrameTableWriter.EndBareList();
  aWriter.TakeAndSplice(mFrameTableWriter.TakeChunkedWriteFunc());
}

void UniqueStacks::SpliceStackTableElements(SpliceableJSONWriter& aWriter) {
  mStackTableWriter.EndBareList();
  aWriter.TakeAndSplice(mStackTableWriter.TakeChunkedWriteFunc());
}

void UniqueStacks::StreamStack(const StackKey& aStack) {
  enum Schema : uint32_t { PREFIX = 0, FRAME = 1 };

  AutoArraySchemaWriter writer(mStackTableWriter);
  if (aStack.mPrefixStackIndex.isSome()) {
    writer.IntElement(PREFIX, *aStack.mPrefixStackIndex);
  }
  writer.IntElement(FRAME, aStack.mFrameIndex);
}

void UniqueStacks::StreamNonJITFrame(const FrameKey& aFrame) {
  using NormalFrameData = FrameKey::NormalFrameData;

  enum Schema : uint32_t {
    LOCATION = 0,
    RELEVANT_FOR_JS = 1,
    INNER_WINDOW_ID = 2,
    IMPLEMENTATION = 3,
    OPTIMIZATIONS = 4,
    LINE = 5,
    COLUMN = 6,
    CATEGORY = 7,
    SUBCATEGORY = 8
  };

  AutoArraySchemaWithStringsWriter writer(mFrameTableWriter, *mUniqueStrings);

  const NormalFrameData& data = aFrame.mData.as<NormalFrameData>();
  writer.StringElement(LOCATION, data.mLocation);
  writer.BoolElement(RELEVANT_FOR_JS, data.mRelevantForJS);

  // It's okay to convert uint64_t to double here because DOM always creates IDs
  // that are convertible to double.
  writer.DoubleElement(INNER_WINDOW_ID, data.mInnerWindowID);

  if (data.mLine.isSome()) {
    writer.IntElement(LINE, *data.mLine);
  }
  if (data.mColumn.isSome()) {
    writer.IntElement(COLUMN, *data.mColumn);
  }
  if (data.mCategoryPair.isSome()) {
    const ProfilingCategoryPairInfo& info =
        GetProfilingCategoryPairInfo(*data.mCategoryPair);
    writer.IntElement(CATEGORY, uint32_t(info.mCategory));
    writer.IntElement(SUBCATEGORY, info.mSubcategoryIndex);
  }
}

struct CStringWriteFunc : public JSONWriteFunc {
  std::string& mBuffer;  // The struct must not outlive this buffer
  explicit CStringWriteFunc(std::string& aBuffer) : mBuffer(aBuffer) {}

  void Write(const Span<const char>& aStr) override {
    mBuffer.append(aStr.data(), aStr.size());
  }
};

struct ProfileSample {
  uint32_t mStack;
  double mTime;
  Maybe<double> mResponsiveness;
};

static void WriteSample(SpliceableJSONWriter& aWriter,
                        const ProfileSample& aSample) {
  enum Schema : uint32_t {
    STACK = 0,
    TIME = 1,
    EVENT_DELAY = 2,
  };

  AutoArraySchemaWriter writer(aWriter);

  writer.IntElement(STACK, aSample.mStack);

  writer.TimeMsElement(TIME, aSample.mTime);

  if (aSample.mResponsiveness.isSome()) {
    writer.DoubleElement(EVENT_DELAY, *aSample.mResponsiveness);
  }
}

class EntryGetter {
 public:
  explicit EntryGetter(ProfileChunkedBuffer::Reader& aReader,
                       uint64_t aInitialReadPos = 0)
      : mBlockIt(
            aReader.At(ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
                aInitialReadPos))),
        mBlockItEnd(aReader.end()) {
    if (!ReadLegacyOrEnd()) {
      // Find and read the next non-legacy entry.
      Next();
    }
  }

  bool Has() const { return mBlockIt != mBlockItEnd; }

  const ProfileBufferEntry& Get() const {
    MOZ_ASSERT(Has(), "Caller should have checked `Has()` before `Get()`");
    return mEntry;
  }

  void Next() {
    MOZ_ASSERT(Has(), "Caller should have checked `Has()` before `Next()`");
    for (;;) {
      ++mBlockIt;
      if (ReadLegacyOrEnd()) {
        // Either we're at the end, or we could read a legacy entry -> Done.
        break;
      }
      // Otherwise loop around until we hit the end or a legacy entry.
    }
  }

  ProfileBufferBlockIndex CurBlockIndex() const {
    return mBlockIt.CurrentBlockIndex();
  }

  uint64_t CurPos() const {
    return CurBlockIndex().ConvertToProfileBufferIndex();
  }

 private:
  // Try to read the entry at the current `mBlockIt` position.
  // * If we're at the end of the buffer, just return `true`.
  // * If there is a "legacy" entry (containing a real `ProfileBufferEntry`),
  //   read it into `mEntry`, and return `true` as well.
  // * Otherwise the entry contains a "modern" type that cannot be read into
  // `mEntry`, return `false` (so `EntryGetter` can skip to another entry).
  bool ReadLegacyOrEnd() {
    if (!Has()) {
      return true;
    }
    // Read the entry "kind", which is always at the start of all entries.
    ProfileBufferEntryReader aER = *mBlockIt;
    auto type = static_cast<ProfileBufferEntry::Kind>(
        aER.ReadObject<ProfileBufferEntry::KindUnderlyingType>());
    MOZ_ASSERT(static_cast<ProfileBufferEntry::KindUnderlyingType>(type) <
               static_cast<ProfileBufferEntry::KindUnderlyingType>(
                   ProfileBufferEntry::Kind::MODERN_LIMIT));
    if (type >= ProfileBufferEntry::Kind::LEGACY_LIMIT) {
      aER.SetRemainingBytes(0);
      return false;
    }
    // Here, we have a legacy item, we need to read it from the start.
    // Because the above `ReadObject` moved the reader, we ned to reset it to
    // the start of the entry before reading the whole entry.
    aER = *mBlockIt;
    aER.ReadBytes(&mEntry, aER.RemainingBytes());
    return true;
  }

  ProfileBufferEntry mEntry;
  ProfileChunkedBuffer::BlockIterator mBlockIt;
  const ProfileChunkedBuffer::BlockIterator mBlockItEnd;
};

// The following grammar shows legal sequences of profile buffer entries.
// The sequences beginning with a ThreadId entry are known as "samples".
//
// (
//   ( /* Samples */
//     ThreadId
//     Time
//     ( NativeLeafAddr
//     | Label FrameFlags? DynamicStringFragment* LineNumber? CategoryPair?
//     | JitReturnAddr
//     )+
//     Responsiveness?
//   )
//   | MarkerData
//   | ( /* Counters */
//       CounterId
//       Time
//       (
//         CounterKey
//         Count
//         Number?
//       )*
//     )
//   | CollectionStart
//   | CollectionEnd
//   | Pause
//   | Resume
//   | ( ProfilerOverheadTime /* Sampling start timestamp */
//       ProfilerOverheadDuration /* Lock acquisition */
//       ProfilerOverheadDuration /* Expired data cleaning */
//       ProfilerOverheadDuration /* Counters */
//       ProfilerOverheadDuration /* Threads */
//     )
// )*
//
// The most complicated part is the stack entry sequence that begins with
// Label. Here are some examples.
//
// - ProfilingStack frames without a dynamic string:
//
//     Label("js::RunScript")
//     CategoryPair(ProfilingCategoryPair::JS)
//
//     Label("XREMain::XRE_main")
//     LineNumber(4660)
//     CategoryPair(ProfilingCategoryPair::OTHER)
//
//     Label("ElementRestyler::ComputeStyleChangeFor")
//     LineNumber(3003)
//     CategoryPair(ProfilingCategoryPair::CSS)
//
// - ProfilingStack frames with a dynamic string:
//
//     Label("nsObserverService::NotifyObservers")
//     FrameFlags(uint64_t(ProfilingStackFrame::Flags::IS_LABEL_FRAME))
//     DynamicStringFragment("domwindo")
//     DynamicStringFragment("wopened")
//     LineNumber(291)
//     CategoryPair(ProfilingCategoryPair::OTHER)
//
//     Label("")
//     FrameFlags(uint64_t(ProfilingStackFrame::Flags::IS_JS_FRAME))
//     DynamicStringFragment("closeWin")
//     DynamicStringFragment("dow (chr")
//     DynamicStringFragment("ome://gl")
//     DynamicStringFragment("obal/con")
//     DynamicStringFragment("tent/glo")
//     DynamicStringFragment("balOverl")
//     DynamicStringFragment("ay.js:5)")
//     DynamicStringFragment("")          # this string holds the closing '\0'
//     LineNumber(25)
//     CategoryPair(ProfilingCategoryPair::JS)
//
//     Label("")
//     FrameFlags(uint64_t(ProfilingStackFrame::Flags::IS_JS_FRAME))
//     DynamicStringFragment("bound (s")
//     DynamicStringFragment("elf-host")
//     DynamicStringFragment("ed:914)")
//     LineNumber(945)
//     CategoryPair(ProfilingCategoryPair::JS)
//
// - A profiling stack frame with an overly long dynamic string:
//
//     Label("")
//     FrameFlags(uint64_t(ProfilingStackFrame::Flags::IS_LABEL_FRAME))
//     DynamicStringFragment("(too lon")
//     DynamicStringFragment("g)")
//     LineNumber(100)
//     CategoryPair(ProfilingCategoryPair::NETWORK)
//
// - A wasm JIT frame:
//
//     Label("")
//     FrameFlags(uint64_t(0))
//     DynamicStringFragment("wasm-fun")
//     DynamicStringFragment("ction[87")
//     DynamicStringFragment("36] (blo")
//     DynamicStringFragment("b:http:/")
//     DynamicStringFragment("/webasse")
//     DynamicStringFragment("mbly.org")
//     DynamicStringFragment("/3dc5759")
//     DynamicStringFragment("4-ce58-4")
//     DynamicStringFragment("626-975b")
//     DynamicStringFragment("-08ad116")
//     DynamicStringFragment("30bc1:38")
//     DynamicStringFragment("29856)")
//
// - A JS frame in a synchronous sample:
//
//     Label("")
//     FrameFlags(uint64_t(ProfilingStackFrame::Flags::IS_LABEL_FRAME))
//     DynamicStringFragment("u (https")
//     DynamicStringFragment("://perf-")
//     DynamicStringFragment("html.io/")
//     DynamicStringFragment("ac0da204")
//     DynamicStringFragment("aaa44d75")
//     DynamicStringFragment("a800.bun")
//     DynamicStringFragment("dle.js:2")
//     DynamicStringFragment("5)")

// Because this is a format entirely internal to the Profiler, any parsing
// error indicates a bug in the ProfileBuffer writing or the parser itself,
// or possibly flaky hardware.
#define ERROR_AND_CONTINUE(msg)                            \
  {                                                        \
    fprintf(stderr, "ProfileBuffer parse error: %s", msg); \
    MOZ_ASSERT(false, msg);                                \
    continue;                                              \
  }

BaseProfilerThreadId ProfileBuffer::StreamSamplesToJSON(
    SpliceableJSONWriter& aWriter, BaseProfilerThreadId aThreadId,
    double aSinceTime, UniqueStacks& aUniqueStacks) const {
  UniquePtr<char[]> dynStrBuf = MakeUnique<char[]>(kMaxFrameKeyLength);

  return mEntries.Read([&](ProfileChunkedBuffer::Reader* aReader) {
    MOZ_ASSERT(aReader,
               "ProfileChunkedBuffer cannot be out-of-session when sampler is "
               "running");

    BaseProfilerThreadId processedThreadId;

    EntryGetter e(*aReader);

    for (;;) {
      // This block skips entries until we find the start of the next sample.
      // This is useful in three situations.
      //
      // - The circular buffer overwrites old entries, so when we start parsing
      //   we might be in the middle of a sample, and we must skip forward to
      //   the start of the next sample.
      //
      // - We skip samples that don't have an appropriate ThreadId or Time.
      //
      // - We skip range Pause, Resume, CollectionStart, Counter and
      //   CollectionEnd entries between samples.
      while (e.Has()) {
        if (e.Get().IsThreadId()) {
          break;
        }
        e.Next();
      }

      if (!e.Has()) {
        break;
      }

      // Due to the skip_to_next_sample block above, if we have an entry here it
      // must be a ThreadId entry.
      MOZ_ASSERT(e.Get().IsThreadId());

      BaseProfilerThreadId threadId = e.Get().GetThreadId();
      e.Next();

      // Ignore samples that are for the wrong thread.
      if (threadId != aThreadId && aThreadId.IsSpecified()) {
        continue;
      }

      MOZ_ASSERT(
          aThreadId.IsSpecified() || !processedThreadId.IsSpecified(),
          "Unspecified aThreadId should only be used with 1-sample buffer");

      ProfileSample sample;

      if (e.Has() && e.Get().IsTime()) {
        sample.mTime = e.Get().GetDouble();
        e.Next();

        // Ignore samples that are too old.
        if (sample.mTime < aSinceTime) {
          continue;
        }
      } else {
        ERROR_AND_CONTINUE("expected a Time entry");
      }

      UniqueStacks::StackKey stack =
          aUniqueStacks.BeginStack(UniqueStacks::FrameKey("(root)"));

      int numFrames = 0;
      while (e.Has()) {
        if (e.Get().IsNativeLeafAddr()) {
          numFrames++;

          void* pc = e.Get().GetPtr();
          e.Next();

          static const uint32_t BUF_SIZE = 256;
          char buf[BUF_SIZE];

          // 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 pcULL = (unsigned long long)(uintptr_t)pc;
          SprintfLiteral(buf, "0x%llx", pcULL);

          // If the "MOZ_PROFILER_SYMBOLICATE" env-var is set, we add a local
          // symbolication description to the PC address. This is off by
          // default, and mainly intended for local development.
          static const bool preSymbolicate = []() {
            const char* symbolicate = getenv("MOZ_PROFILER_SYMBOLICATE");
            return symbolicate && symbolicate[0] != '\0';
          }();
          if (preSymbolicate) {
            MozCodeAddressDetails details;
            if (MozDescribeCodeAddress(pc, &details)) {
              // Replace \0 terminator with space.
              const uint32_t pcLen = strlen(buf);
              buf[pcLen] = ' ';
              // Add description after space. Note: Using a frame number of 0,
              // as using `numFrames` wouldn't help here, and would prevent
              // combining same function calls that happen at different depths.
              // TODO: Remove unsightly "#00: " if too annoying. :-)
              MozFormatCodeAddressDetails(
                  buf + pcLen + 1, BUF_SIZE - (pcLen + 1), 0, pc, &details);
            }
          }

          stack = aUniqueStacks.AppendFrame(stack, UniqueStacks::FrameKey(buf));

        } else if (e.Get().IsLabel()) {
          numFrames++;

          const char* label = e.Get().GetString();
          e.Next();

          using FrameFlags = ProfilingStackFrame::Flags;
          uint32_t frameFlags = 0;
          if (e.Has() && e.Get().IsFrameFlags()) {
            frameFlags = uint32_t(e.Get().GetUint64());
            e.Next();
          }

          bool relevantForJS =
              frameFlags & uint32_t(FrameFlags::RELEVANT_FOR_JS);

          // Copy potential dynamic string fragments into dynStrBuf, so that
          // dynStrBuf will then contain the entire dynamic string.
          size_t i = 0;
          dynStrBuf[0] = '\0';
          while (e.Has()) {
            if (e.Get().IsDynamicStringFragment()) {
              char chars[ProfileBufferEntry::kNumChars];
              e.Get().CopyCharsInto(chars);
              for (char c : chars) {
                if (i < kMaxFrameKeyLength) {
                  dynStrBuf[i] = c;
                  i++;
                }
              }
              e.Next();
            } else {
              break;
            }
          }
          dynStrBuf[kMaxFrameKeyLength - 1] = '\0';
          bool hasDynamicString = (i != 0);

          std::string frameLabel;
          if (label[0] != '\0' && hasDynamicString) {
            if (frameFlags & uint32_t(FrameFlags::STRING_TEMPLATE_METHOD)) {
              frameLabel += label;
              frameLabel += '.';
              frameLabel += dynStrBuf.get();
            } else if (frameFlags &
                       uint32_t(FrameFlags::STRING_TEMPLATE_GETTER)) {
              frameLabel += "get ";
              frameLabel += label;
              frameLabel += '.';
              frameLabel += dynStrBuf.get();
            } else if (frameFlags &
                       uint32_t(FrameFlags::STRING_TEMPLATE_SETTER)) {
              frameLabel += "set ";
              frameLabel += label;
              frameLabel += '.';
              frameLabel += dynStrBuf.get();
            } else {
              frameLabel += label;
              frameLabel += ' ';
              frameLabel += dynStrBuf.get();
            }
          } else if (hasDynamicString) {
            frameLabel += dynStrBuf.get();
          } else {
            frameLabel += label;
          }

          uint64_t innerWindowID = 0;
          if (e.Has() && e.Get().IsInnerWindowID()) {
            innerWindowID = uint64_t(e.Get().GetUint64());
            e.Next();
          }

          Maybe<unsigned> line;
          if (e.Has() && e.Get().IsLineNumber()) {
            line = Some(unsigned(e.Get().GetInt()));
            e.Next();
          }

          Maybe<unsigned> column;
          if (e.Has() && e.Get().IsColumnNumber()) {
            column = Some(unsigned(e.Get().GetInt()));
            e.Next();
          }

          Maybe<ProfilingCategoryPair> categoryPair;
          if (e.Has() && e.Get().IsCategoryPair()) {
            categoryPair =
                Some(ProfilingCategoryPair(uint32_t(e.Get().GetInt())));
            e.Next();
          }

          stack = aUniqueStacks.AppendFrame(
              stack, UniqueStacks::FrameKey(std::move(frameLabel),
                                            relevantForJS, innerWindowID, line,
                                            column, categoryPair));

        } else {
          break;
        }
      }

      if (numFrames == 0) {
        // It is possible to have empty stacks if native stackwalking is
        // disabled. Skip samples with empty stacks. (See Bug 1497985).
        // Thus, don't use ERROR_AND_CONTINUE, but just continue.
        continue;
      }

      sample.mStack = aUniqueStacks.GetOrAddStackIndex(stack);

      if (e.Has() && e.Get().IsResponsiveness()) {
        sample.mResponsiveness = Some(e.Get().GetDouble());
        e.Next();
      }

      WriteSample(aWriter, sample);

      processedThreadId = threadId;
    }

    return processedThreadId;
  });
}

void ProfileBuffer::StreamMarkersToJSON(SpliceableJSONWriter& aWriter,
                                        BaseProfilerThreadId aThreadId,
                                        const TimeStamp& aProcessStartTime,
                                        double aSinceTime,
                                        UniqueStacks& aUniqueStacks) const {
  mEntries.ReadEach([&](ProfileBufferEntryReader& aER) {
    auto type = static_cast<ProfileBufferEntry::Kind>(
        aER.ReadObject<ProfileBufferEntry::KindUnderlyingType>());
    MOZ_ASSERT(static_cast<ProfileBufferEntry::KindUnderlyingType>(type) <
               static_cast<ProfileBufferEntry::KindUnderlyingType>(
                   ProfileBufferEntry::Kind::MODERN_LIMIT));
    if (type == ProfileBufferEntry::Kind::Marker) {
      ::mozilla::base_profiler_markers_detail::DeserializeAfterKindAndStream(
          aER,
          [&](const BaseProfilerThreadId& aMarkerThreadId) {
            return (aMarkerThreadId == aThreadId) ? &aWriter : nullptr;
          },
          [&](ProfileChunkedBuffer& aChunkedBuffer) {
            ProfilerBacktrace backtrace("", &aChunkedBuffer);
            backtrace.StreamJSON(aWriter, TimeStamp::ProcessCreation(),
                                 aUniqueStacks);
          },
          // We don't have Rust markers in the mozglue.
          [&](mozilla::base_profiler_markers_detail::Streaming::
                  DeserializerTag) {
            MOZ_ASSERT_UNREACHABLE("No Rust markers in mozglue.");
          });
    } else {
      // The entry was not a marker, we need to skip to the end.
      aER.SetRemainingBytes(0);
    }
  });
}

void ProfileBuffer::StreamProfilerOverheadToJSON(
    SpliceableJSONWriter& aWriter, const TimeStamp& aProcessStartTime,
    double aSinceTime) const {
  mEntries.Read([&](ProfileChunkedBuffer::Reader* aReader) {
    MOZ_ASSERT(aReader,
               "ProfileChunkedBuffer cannot be out-of-session when sampler is "
               "running");

    EntryGetter e(*aReader);

    enum Schema : uint32_t {
      TIME = 0,
      LOCKING = 1,
      MARKER_CLEANING = 2,
      COUNTERS = 3,
      THREADS = 4
    };

    aWriter.StartObjectProperty("profilerOverhead");
    aWriter.StartObjectProperty("samples");
    // Stream all sampling overhead data. We skip other entries, because we
    // process them in StreamSamplesToJSON()/etc.
    {
      JSONSchemaWriter schema(aWriter);
      schema.WriteField("time");
      schema.WriteField("locking");
      schema.WriteField("expiredMarkerCleaning");
      schema.WriteField("counters");
      schema.WriteField("threads");
    }

    aWriter.StartArrayProperty("data");
    double firstTime = 0.0;
    double lastTime = 0.0;
    ProfilerStats intervals, overheads, lockings, cleanings, counters, threads;
    while (e.Has()) {
      // valid sequence: ProfilerOverheadTime, ProfilerOverheadDuration * 4
      if (e.Get().IsProfilerOverheadTime()) {
        double time = e.Get().GetDouble();
        if (time >= aSinceTime) {
          e.Next();
          if (!e.Has() || !e.Get().IsProfilerOverheadDuration()) {
            ERROR_AND_CONTINUE(
                "expected a ProfilerOverheadDuration entry after "
                "ProfilerOverheadTime");
          }
          double locking = e.Get().GetDouble();
          e.Next();
          if (!e.Has() || !e.Get().IsProfilerOverheadDuration()) {
            ERROR_AND_CONTINUE(
                "expected a ProfilerOverheadDuration entry after "
                "ProfilerOverheadTime,ProfilerOverheadDuration");
          }
          double cleaning = e.Get().GetDouble();
          e.Next();
          if (!e.Has() || !e.Get().IsProfilerOverheadDuration()) {
            ERROR_AND_CONTINUE(
                "expected a ProfilerOverheadDuration entry after "
                "ProfilerOverheadTime,ProfilerOverheadDuration*2");
          }
          double counter = e.Get().GetDouble();
          e.Next();
          if (!e.Has() || !e.Get().IsProfilerOverheadDuration()) {
            ERROR_AND_CONTINUE(
                "expected a ProfilerOverheadDuration entry after "
                "ProfilerOverheadTime,ProfilerOverheadDuration*3");
          }
          double thread = e.Get().GetDouble();

          if (firstTime == 0.0) {
            firstTime = time;
          } else {
            // Note that we'll have 1 fewer interval than other numbers (because
            // we need both ends of an interval to know its duration). The final
            // difference should be insignificant over the expected many
            // thousands of iterations.
            intervals.Count(time - lastTime);
          }
          lastTime = time;
          overheads.Count(locking + cleaning + counter + thread);
          lockings.Count(locking);
          cleanings.Count(cleaning);
          counters.Count(counter);
          threads.Count(thread);

          AutoArraySchemaWriter writer(aWriter);
          writer.TimeMsElement(TIME, time);
          writer.DoubleElement(LOCKING, locking);
          writer.DoubleElement(MARKER_CLEANING, cleaning);
          writer.DoubleElement(COUNTERS, counter);
          writer.DoubleElement(THREADS, thread);
        }
      }
      e.Next();
    }
    aWriter.EndArray();   // data
    aWriter.EndObject();  // samples

    // Only output statistics if there is at least one full interval (and
    // therefore at least two samplings.)
    if (intervals.n > 0) {
      aWriter.StartObjectProperty("statistics");
      aWriter.DoubleProperty("profiledDuration", lastTime - firstTime);
      aWriter.IntProperty("samplingCount", overheads.n);
      aWriter.DoubleProperty("overheadDurations", overheads.sum);
      aWriter.DoubleProperty("overheadPercentage",
                             overheads.sum / (lastTime - firstTime));
#define PROFILER_STATS(name, var)                           \
  aWriter.DoubleProperty("mean" name, (var).sum / (var).n); \
  aWriter.DoubleProperty("min" name, (var).min);            \
  aWriter.DoubleProperty("max" name, (var).max);
      PROFILER_STATS("Interval", intervals);
      PROFILER_STATS("Overhead", overheads);
      PROFILER_STATS("Lockings", lockings);
      PROFILER_STATS("Cleaning", cleanings);
      PROFILER_STATS("Counter", counters);
      PROFILER_STATS("Thread", threads);
#undef PROFILER_STATS
      aWriter.EndObject();  // statistics
    }
    aWriter.EndObject();  // profilerOverhead
  });
}

struct CounterKeyedSample {
  double mTime;
  uint64_t mNumber;
  int64_t mCount;
};

using CounterKeyedSamples = Vector<CounterKeyedSample>;

using CounterMap = HashMap<uint64_t, CounterKeyedSamples>;

// HashMap lookup, if not found, a default value is inserted.
// Returns reference to (existing or new) value inside the HashMap.
template <typename HashM, typename Key>
static auto& LookupOrAdd(HashM& aMap, Key&& aKey) {
  auto addPtr = aMap.lookupForAdd(aKey);
  if (!addPtr) {
    MOZ_RELEASE_ASSERT(aMap.add(addPtr, std::forward<Key>(aKey),
                                typename HashM::Entry::ValueType{}));
    MOZ_ASSERT(!!addPtr);
  }
  return addPtr->value();
}

void ProfileBuffer::StreamCountersToJSON(SpliceableJSONWriter& aWriter,
                                         const TimeStamp& aProcessStartTime,
                                         double aSinceTime) const {
  // Because this is a format entirely internal to the Profiler, any parsing
  // error indicates a bug in the ProfileBuffer writing or the parser itself,
  // or possibly flaky hardware.

  mEntries.Read([&](ProfileChunkedBuffer::Reader* aReader) {
    MOZ_ASSERT(aReader,
               "ProfileChunkedBuffer cannot be out-of-session when sampler is "
               "running");

    EntryGetter e(*aReader);

    enum Schema : uint32_t { TIME = 0, NUMBER = 1, COUNT = 2 };

    // Stream all counters. We skip other entries, because we process them in
    // StreamSamplesToJSON()/etc.
    //
    // Valid sequence in the buffer:
    // CounterID
    // Time
    // ( CounterKey Count Number? )*
    //
    // And the JSON (example):
    // "counters": {
    //  "name": "malloc",
    //  "category": "Memory",
    //  "description": "Amount of allocated memory",
    //  "sample_groups": {
    //   "id": 0,
    //   "samples": {
    //    "schema": {"time": 0, "number": 1, "count": 2},
    //    "data": [
    //     [
    //      16117.033968000002,
    //      2446216,
    //      6801320
    //     ],
    //     [
    //      16118.037638,
    //      2446216,
    //      6801320
    //     ],
    //    ],
    //   }
    //  }
    // },

    // Build the map of counters and populate it
    HashMap<void*, CounterMap> counters;

    while (e.Has()) {
      // skip all non-Counters, including if we start in the middle of a counter
      if (e.Get().IsCounterId()) {
        void* id = e.Get().GetPtr();
        CounterMap& counter = LookupOrAdd(counters, id);
        e.Next();
        if (!e.Has() || !e.Get().IsTime()) {
          ERROR_AND_CONTINUE("expected a Time entry");
        }
        double time = e.Get().GetDouble();
        if (time >= aSinceTime) {
          e.Next();
          while (e.Has() && e.Get().IsCounterKey()) {
            uint64_t key = e.Get().GetUint64();
            CounterKeyedSamples& data = LookupOrAdd(counter, key);
            e.Next();
            if (!e.Has() || !e.Get().IsCount()) {
              ERROR_AND_CONTINUE("expected a Count entry");
            }
            int64_t count = e.Get().GetUint64();
            e.Next();
            uint64_t number;
            if (!e.Has() || !e.Get().IsNumber()) {
              number = 0;
            } else {
              number = e.Get().GetInt64();
            }
            CounterKeyedSample sample = {time, number, count};
            MOZ_RELEASE_ASSERT(data.append(sample));
          }
        } else {
          // skip counter sample - only need to skip the initial counter
          // id, then let the loop at the top skip the rest
        }
      }
      e.Next();
    }
    // we have a map of a map of counter entries; dump them to JSON
    if (counters.count() == 0) {
      return;
    }

    aWriter.StartArrayProperty("counters");
    for (auto iter = counters.iter(); !iter.done(); iter.next()) {
      CounterMap& counter = iter.get().value();
      const BaseProfilerCount* base_counter =
          static_cast<const BaseProfilerCount*>(iter.get().key());

      aWriter.Start();
      aWriter.StringProperty("name", MakeStringSpan(base_counter->mLabel));
      aWriter.StringProperty("category",
                             MakeStringSpan(base_counter->mCategory));
      aWriter.StringProperty("description",
                             MakeStringSpan(base_counter->mDescription));

      aWriter.StartArrayProperty("sample_groups");
      for (auto counter_iter = counter.iter(); !counter_iter.done();
           counter_iter.next()) {
        CounterKeyedSamples& samples = counter_iter.get().value();
        uint64_t key = counter_iter.get().key();

        size_t size = samples.length();
        if (size == 0) {
          continue;
        }

        aWriter.StartObjectElement();
        {
          aWriter.IntProperty("id", static_cast<int64_t>(key));
          aWriter.StartObjectProperty("samples");
          {
            // XXX Can we assume a missing count means 0?
            JSONSchemaWriter schema(aWriter);
            schema.WriteField("time");
            schema.WriteField("number");
            schema.WriteField("count");
          }

          aWriter.StartArrayProperty("data");
          uint64_t previousNumber = 0;
          int64_t previousCount = 0;
          for (size_t i = 0; i < size; i++) {
            // Encode as deltas, and only encode if different than the last
            // sample
            if (i == 0 || samples[i].mNumber != previousNumber ||
                samples[i].mCount != previousCount) {
              MOZ_ASSERT(i == 0 || samples[i].mTime >= samples[i - 1].mTime);
              MOZ_ASSERT(samples[i].mNumber >= previousNumber);
              MOZ_ASSERT(samples[i].mNumber - previousNumber <=
                         uint64_t(std::numeric_limits<int64_t>::max()));

              AutoArraySchemaWriter writer(aWriter);
              writer.TimeMsElement(TIME, samples[i].mTime);
              writer.IntElement(
                  NUMBER,
                  static_cast<int64_t>(samples[i].mNumber - previousNumber));
              writer.IntElement(COUNT, samples[i].mCount - previousCount);
              previousNumber = samples[i].mNumber;
              previousCount = samples[i].mCount;
            }
          }
          aWriter.EndArray();   // data
          aWriter.EndObject();  // samples
        }
        aWriter.EndObject();  // sample_groups item
      }
      aWriter.EndArray();  // sample groups
      aWriter.End();       // for each counter
    }
    aWriter.EndArray();  // counters
  });
}

#undef ERROR_AND_CONTINUE

static void AddPausedRange(SpliceableJSONWriter& aWriter, const char* aReason,
                           const Maybe<double>& aStartTime,
                           const Maybe<double>& aEndTime) {
  aWriter.Start();
  if (aStartTime) {
    aWriter.TimeDoubleMsProperty("startTime", *aStartTime);
  } else {
    aWriter.NullProperty("startTime");
  }
  if (aEndTime) {
    aWriter.TimeDoubleMsProperty("endTime", *aEndTime);
  } else {
    aWriter.NullProperty("endTime");
  }
  aWriter.StringProperty("reason", MakeStringSpan(aReason));
  aWriter.End();
}

void ProfileBuffer::StreamPausedRangesToJSON(SpliceableJSONWriter& aWriter,
                                             double aSinceTime) const {
  mEntries.Read([&](ProfileChunkedBuffer::Reader* aReader) {
    MOZ_ASSERT(aReader,
               "ProfileChunkedBuffer cannot be out-of-session when sampler is "
               "running");

    EntryGetter e(*aReader);

    Maybe<double> currentPauseStartTime;
    Maybe<double> currentCollectionStartTime;

    while (e.Has()) {
      if (e.Get().IsPause()) {
        currentPauseStartTime = Some(e.Get().GetDouble());
      } else if (e.Get().IsResume()) {
        AddPausedRange(aWriter, "profiler-paused", currentPauseStartTime,
                       Some(e.Get().GetDouble()));
        currentPauseStartTime = Nothing();
      } else if (e.Get().IsCollectionStart()) {
        currentCollectionStartTime = Some(e.Get().GetDouble());
      } else if (e.Get().IsCollectionEnd()) {
        AddPausedRange(aWriter, "collecting", currentCollectionStartTime,
                       Some(e.Get().GetDouble()));
        currentCollectionStartTime = Nothing();
      }
      e.Next();
    }

    if (currentPauseStartTime) {
      AddPausedRange(aWriter, "profiler-paused", currentPauseStartTime,
                     Nothing());
    }
    if (currentCollectionStartTime) {
      AddPausedRange(aWriter, "collecting", currentCollectionStartTime,
                     Nothing());
    }
  });
}

bool ProfileBuffer::DuplicateLastSample(BaseProfilerThreadId aThreadId,
                                        const TimeStamp& aProcessStartTime,
                                        Maybe<uint64_t>& aLastSample) {
  if (!aLastSample) {
    return false;
  }

  ProfileChunkedBuffer tempBuffer(
      ProfileChunkedBuffer::ThreadSafety::WithoutMutex, mWorkerChunkManager);

  auto retrieveWorkerChunk = MakeScopeExit(
      [&]() { mWorkerChunkManager.Reset(tempBuffer.GetAllChunks()); });

  const bool ok = mEntries.Read([&](ProfileChunkedBuffer::Reader* aReader) {
    MOZ_ASSERT(aReader,
               "ProfileChunkedBuffer cannot be out-of-session when sampler is "
               "running");

    EntryGetter e(*aReader, *aLastSample);

    if (e.CurPos() != *aLastSample) {
      // The last sample is no longer within the buffer range, so we cannot
      // use it. Reset the stored buffer position to Nothing().
      aLastSample.reset();
      return false;
    }

    MOZ_RELEASE_ASSERT(e.Has() && e.Get().IsThreadId() &&
                       e.Get().GetThreadId() == aThreadId);

    e.Next();

    // Go through the whole entry and duplicate it, until we find the next
    // one.
    while (e.Has()) {
      switch (e.Get().GetKind()) {
        case ProfileBufferEntry::Kind::Pause:
        case ProfileBufferEntry::Kind::Resume:
        case ProfileBufferEntry::Kind::PauseSampling:
        case ProfileBufferEntry::Kind::ResumeSampling:
        case ProfileBufferEntry::Kind::CollectionStart:
        case ProfileBufferEntry::Kind::CollectionEnd:
        case ProfileBufferEntry::Kind::ThreadId:
          // We're done.
          return true;
        case ProfileBufferEntry::Kind::Time:
          // Copy with new time
          AddEntry(
              tempBuffer,
              ProfileBufferEntry::Time(
                  (TimeStamp::Now() - aProcessStartTime).ToMilliseconds()));
          break;
        case ProfileBufferEntry::Kind::CounterKey:
        case ProfileBufferEntry::Kind::Number:
        case ProfileBufferEntry::Kind::Count:
        case ProfileBufferEntry::Kind::Responsiveness:
          // Don't copy anything not part of a thread's stack sample
          break;
        case ProfileBufferEntry::Kind::CounterId:
          // CounterId is normally followed by Time - if so, we'd like
          // to skip it.  If we duplicate Time, it won't hurt anything, just
          // waste buffer space (and this can happen if the CounterId has
          // fallen off the end of the buffer, but Time (and Number/Count)
          // are still in the buffer).
          e.Next();
          if (e.Has() && e.Get().GetKind() != ProfileBufferEntry::Kind::Time) {
            // this would only happen if there was an invalid sequence
            // in the buffer.  Don't skip it.
            continue;
          }
          // we've skipped Time
          break;
        case ProfileBufferEntry::Kind::ProfilerOverheadTime:
          // ProfilerOverheadTime is normally followed by
          // ProfilerOverheadDuration*4 - if so, we'd like to skip it. Don't
          // duplicate, as we are in the middle of a sampling and will soon
          // capture its own overhead.
          e.Next();
          // A missing Time would only happen if there was an invalid
          // sequence in the buffer. Don't skip unexpected entry.
          if (e.Has() &&
              e.Get().GetKind() !=
                  ProfileBufferEntry::Kind::ProfilerOverheadDuration) {
            continue;
          }
          e.Next();
          if (e.Has() &&
              e.Get().GetKind() !=
                  ProfileBufferEntry::Kind::ProfilerOverheadDuration) {
            continue;
          }
          e.Next();
          if (e.Has() &&
              e.Get().GetKind() !=
                  ProfileBufferEntry::Kind::ProfilerOverheadDuration) {
            continue;
          }
          e.Next();
          if (e.Has() &&
              e.Get().GetKind() !=
                  ProfileBufferEntry::Kind::ProfilerOverheadDuration) {
            continue;
          }
          // we've skipped ProfilerOverheadTime and
          // ProfilerOverheadDuration*4.
          break;
        default: {
          // Copy anything else we don't know about.
          AddEntry(tempBuffer, e.Get());
          break;
        }
      }
      e.Next();
    }
    return true;
  });

  if (!ok) {
    return false;
  }

  // If the buffer was big enough, there won't be any cleared blocks.
  if (tempBuffer.GetState().mClearedBlockCount != 0) {
    // No need to try to read stack again as it won't fit. Reset the stored
    // buffer position to Nothing().
    aLastSample.reset();
    return false;
  }

  aLastSample = Some(AddThreadIdEntry(aThreadId));

  tempBuffer.Read([&](ProfileChunkedBuffer::Reader* aReader) {
    MOZ_ASSERT(aReader, "tempBuffer cannot be out-of-session");

    EntryGetter e(*aReader);

    while (e.Has()) {
      AddEntry(e.Get());
      e.Next();
    }
  });

  return true;
}

void ProfileBuffer::DiscardSamplesBeforeTime(double aTime) {
  // This function does nothing!
  // The duration limit will be removed from Firefox, see bug 1632365.
  Unused << aTime;
}

// END ProfileBuffer
////////////////////////////////////////////////////////////////////////

}  // namespace baseprofiler
}  // namespace mozilla