Quellcodebibliothek Statistik Leitseite products/sources/formale Sprachen/C/Firefox/mozglue/baseprofiler/public/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 61 kB image not shown  

Quelle  ProfileChunkedBuffer.h   Sprache: C

 
/* -*- Mode: C++; tab-width: 2; 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/. */


#ifndef ProfileChunkedBuffer_h
#define ProfileChunkedBuffer_h

#include "mozilla/Attributes.h"
#include "mozilla/BaseProfilerDetail.h"
#include "mozilla/NotNull.h"
#include "mozilla/ProfileBufferChunkManager.h"
#include "mozilla/ProfileBufferChunkManagerSingle.h"
#include "mozilla/ProfileBufferEntrySerialization.h"
#include "mozilla/ProfileChunkedBufferDetail.h"
#include "mozilla/RefPtr.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Unused.h"

#include <utility>

#ifdef DEBUG
#  include <cstdio>
#endif

namespace mozilla {

// Thread-safe buffer that can store blocks of different sizes during defined
// sessions, using Chunks (from a ChunkManager) as storage.
//
// Each *block* contains an *entry* and the entry size:
// [ entry_size | entry ] [ entry_size | entry ] ...
//
// *In-session* is a period of time during which `ProfileChunkedBuffer` allows
// reading and writing.
// *Out-of-session*, the `ProfileChunkedBuffer` object is still valid, but
// contains no data, and gracefully denies accesses.
//
// To write an entry, the buffer reserves a block of sufficient size (to contain
// user data of predetermined size), writes the entry size, and lets the caller
// fill the entry contents using a ProfileBufferEntryWriter. E.g.:
// ```
// ProfileChunkedBuffer cb(...);
// cb.ReserveAndPut([]() { return sizeof(123); },
//                  [&](Maybe<ProfileBufferEntryWriter>& aEW) {
//                    if (aEW) { aEW->WriteObject(123); }
//                  });
// ```
// Other `Put...` functions may be used as shortcuts for simple entries.
// The objects given to the caller's callbacks should only be used inside the
// callbacks and not stored elsewhere, because they keep their own references to
// chunk memory and therefore should not live longer.
// Different type of objects may be serialized into an entry, see
// `ProfileBufferEntryWriter::Serializer` for more information.
//
// When reading data, the buffer iterates over blocks (it knows how to read the
// entry size, and therefore move to the next block), and lets the caller read
// the entry inside of each block. E.g.:
// ```
// cb.ReadEach([](ProfileBufferEntryReader& aER) {
//   /* Use ProfileBufferEntryReader functions to read serialized objects. */
//   int n = aER.ReadObject<int>();
// });
// ```
// Different type of objects may be deserialized from an entry, see
// `ProfileBufferEntryReader::Deserializer` for more information.
//
// Writers may retrieve the block index corresponding to an entry
// (`ProfileBufferBlockIndex` is an opaque type preventing the user from easily
// modifying it). That index may later be used with `ReadAt` to get back to the
// entry in that particular block -- if it still exists.
class ProfileChunkedBuffer {
 public:
  using Byte = ProfileBufferChunk::Byte;
  using Length = ProfileBufferChunk::Length;

  enum class ThreadSafety { WithoutMutex, WithMutex };

  // Default constructor starts out-of-session (nothing to read or write).
  explicit ProfileChunkedBuffer(ThreadSafety aThreadSafety)
      : mMutex(aThreadSafety != ThreadSafety::WithoutMutex) {}

  // Start in-session with external chunk manager.
  ProfileChunkedBuffer(ThreadSafety aThreadSafety,
                       ProfileBufferChunkManager& aChunkManager)
      : mMutex(aThreadSafety != ThreadSafety::WithoutMutex) {
    SetChunkManager(aChunkManager);
  }

  // Start in-session with owned chunk manager.
  ProfileChunkedBuffer(ThreadSafety aThreadSafety,
                       UniquePtr<ProfileBufferChunkManager>&& aChunkManager)
      : mMutex(aThreadSafety != ThreadSafety::WithoutMutex) {
    SetChunkManager(std::move(aChunkManager));
  }

  ~ProfileChunkedBuffer() {
    // Do proper clean-up by resetting the chunk manager.
    ResetChunkManager();
  }

  // This cannot change during the lifetime of this buffer, so there's no need
  // to lock.
  [[nodiscard]] bool IsThreadSafe() const { return mMutex.IsActivated(); }

  [[nodiscard]] bool IsInSession() const {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    return !!mChunkManager;
  }

  // Stop using the current chunk manager.
  // If we own the current chunk manager, it will be destroyed.
  // This will always clear currently-held chunks, if any.
  void ResetChunkManager() {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    Unused << ResetChunkManager(lock);
  }

  // Set the current chunk manager.
  // The caller is responsible for keeping the chunk manager alive as along as
  // it's used here (until the next (Re)SetChunkManager, or
  // ~ProfileChunkedBuffer).
  void SetChunkManager(ProfileBufferChunkManager& aChunkManager) {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    Unused << ResetChunkManager(lock);
    SetChunkManager(aChunkManager, lock);
  }

  // Set the current chunk manager, and keep ownership of it.
  void SetChunkManager(UniquePtr<ProfileBufferChunkManager>&& aChunkManager) {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    Unused << ResetChunkManager(lock);
    mOwnedChunkManager = std::move(aChunkManager);
    if (mOwnedChunkManager) {
      SetChunkManager(*mOwnedChunkManager, lock);
    }
  }

  // Set the current chunk manager, except if it's already the one provided.
  // The caller is responsible for keeping the chunk manager alive as along as
  // it's used here (until the next (Re)SetChunkManager, or
  // ~ProfileChunkedBuffer).
  void SetChunkManagerIfDifferent(ProfileBufferChunkManager& aChunkManager) {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    if (!mChunkManager || mChunkManager != &aChunkManager) {
      Unused << ResetChunkManager(lock);
      SetChunkManager(aChunkManager, lock);
    }
  }

  // Clear the contents of this buffer, ready to receive new chunks.
  // Note that memory is not freed: No chunks are destroyed, they are all
  // receycled.
  // Also the range doesn't reset, instead it continues at some point after the
  // previous range. This may be useful if the caller may be keeping indexes
  // into old chunks that have now been cleared, using these indexes will fail
  // gracefully (instead of potentially pointing into new data).
  void Clear() {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    if (MOZ_UNLIKELY(!mChunkManager)) {
      // Out-of-session.
      return;
    }

    mRangeStart = mRangeEnd = mNextChunkRangeStart;
    mPushedBlockCount = 0;
    mClearedBlockCount = 0;
    mFailedPutBytes = 0;

    // Recycle all released chunks as "next" chunks. This will reduce the number
    // of future allocations. Also, when using ProfileBufferChunkManagerSingle,
    // this retrieves the one chunk if it was released.
    UniquePtr<ProfileBufferChunk> releasedChunks =
        mChunkManager->GetExtantReleasedChunks();
    if (releasedChunks) {
      // Released chunks should be in the "Done" state, they need to be marked
      // "recycled" before they can be reused.
      for (ProfileBufferChunk* chunk = releasedChunks.get(); chunk;
           chunk = chunk->GetNext()) {
        chunk->MarkRecycled();
      }
      mNextChunks = ProfileBufferChunk::Join(std::move(mNextChunks),
                                             std::move(releasedChunks));
    }

    if (mCurrentChunk) {
      // We already have a current chunk (empty or in-use), mark it "done" and
      // then "recycled", ready to be reused.
      mCurrentChunk->MarkDone();
      mCurrentChunk->MarkRecycled();
    } else {
      if (!mNextChunks) {
        // No current chunk, and no next chunks to recycle, nothing more to do.
        // The next "Put" operation will try to allocate a chunk as needed.
        return;
      }

      // No current chunk, take a next chunk.
      mCurrentChunk = std::exchange(mNextChunks, mNextChunks->ReleaseNext());
    }

    // Here, there was already a current chunk, or one has just been taken.
    // Make sure it's ready to receive new entries.
    InitializeCurrentChunk(lock);
  }

  // Buffer maximum length in bytes.
  Maybe<size_t> BufferLength() const {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    if (!mChunkManager) {
      return Nothing{};
    }
    return Some(mChunkManager->MaxTotalSize());
  }

  [[nodiscard]] size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    return SizeOfExcludingThis(aMallocSizeOf, lock);
  }

  [[nodiscard]] size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf, lock);
  }

  // Snapshot of the buffer state.
  struct State {
    // Index to/before the first block.
    ProfileBufferIndex mRangeStart = 1;

    // Index past the last block. Equals mRangeStart if empty.
    ProfileBufferIndex mRangeEnd = 1;

    // Number of blocks that have been pushed into this buffer.
    uint64_t mPushedBlockCount = 0;

    // Number of blocks that have been removed from this buffer.
    // Note: Live entries = pushed - cleared.
    uint64_t mClearedBlockCount = 0;

    // Number of bytes that could not be put into this buffer.
    uint64_t mFailedPutBytes = 0;
  };

  // Get a snapshot of the current state.
  // When out-of-session, mFirstReadIndex==mNextWriteIndex, and
  // mPushedBlockCount==mClearedBlockCount==0.
  // Note that these may change right after this thread-safe call, so they
  // should only be used for statistical purposes.
  [[nodiscard]] State GetState() const {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    return {mRangeStart, mRangeEnd, mPushedBlockCount, mClearedBlockCount,
            mFailedPutBytes};
  }

  // In in-session, return the start TimeStamp of the earliest chunk.
  // If out-of-session, return a null TimeStamp.
  [[nodiscard]] TimeStamp GetEarliestChunkStartTimeStamp() const {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    if (MOZ_UNLIKELY(!mChunkManager)) {
      // Out-of-session.
      return {};
    }
    return mChunkManager->PeekExtantReleasedChunks(
        [&](const ProfileBufferChunk* aOldestChunk) -> TimeStamp {
          if (aOldestChunk) {
            return aOldestChunk->ChunkHeader().mStartTimeStamp;
          }
          if (mCurrentChunk) {
            return mCurrentChunk->ChunkHeader().mStartTimeStamp;
          }
          return {};
        });
  }

  [[nodiscard]] bool IsEmpty() const {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    return mRangeStart == mRangeEnd;
  }

  // True if this buffer is already locked on this thread.
  // This should be used if some functions may call an already-locked buffer,
  // e.g.: Put -> memory hook -> profiler_add_native_allocation_marker -> Put.
  [[nodiscard]] bool IsThreadSafeAndLockedOnCurrentThread() const {
    return mMutex.IsActivatedAndLockedOnCurrentThread();
  }

  // Lock the buffer mutex and run the provided callback.
  // This can be useful when the caller needs to explicitly lock down this
  // buffer, but not do anything else with it.
  template <typename Callback>
  auto LockAndRun(Callback&& aCallback) const {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    return std::forward<Callback>(aCallback)();
  }

  // Reserve a block that can hold an entry of the given `aCallbackEntryBytes()`
  // size, write the entry size (ULEB128-encoded), and invoke and return
  // `aCallback(Maybe<ProfileBufferEntryWriter>&)`.
  // Note: `aCallbackEntryBytes` is a callback instead of a simple value, to
  // delay this potentially-expensive computation until after we're checked that
  // we're in-session; use `Put(Length, Callback)` below if you know the size
  // already.
  template <typename CallbackEntryBytes, typename Callback>
  auto ReserveAndPut(CallbackEntryBytes&& aCallbackEntryBytes,
                     Callback&& aCallback)
      -> decltype(std::forward<Callback>(aCallback)(
          std::declval<Maybe<ProfileBufferEntryWriter>&>())) {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);

    // This can only be read in the 2nd lambda below after it has been written
    // by the first lambda.
    Length entryBytes;

    return ReserveAndPutRaw(
        [&]() {
          entryBytes = std::forward<CallbackEntryBytes>(aCallbackEntryBytes)();
          MOZ_ASSERT(entryBytes != 0, "Empty entries are not allowed");
          return ULEB128Size(entryBytes) + entryBytes;
        },
        [&](Maybe<ProfileBufferEntryWriter>& aMaybeEntryWriter) {
          if (aMaybeEntryWriter.isSome()) {
            aMaybeEntryWriter->WriteULEB128(entryBytes);
            MOZ_ASSERT(aMaybeEntryWriter->RemainingBytes() == entryBytes);
          }
          return std::forward<Callback>(aCallback)(aMaybeEntryWriter);
        },
        lock);
  }

  template <typename Callback>
  auto Put(Length aEntryBytes, Callback&& aCallback) {
    return ReserveAndPut([aEntryBytes]() { return aEntryBytes; },
                         std::forward<Callback>(aCallback));
  }

  // Add a new entry copied from the given buffer, return block index.
  ProfileBufferBlockIndex PutFrom(const void* aSrc, Length aBytes) {
    return ReserveAndPut(
        [aBytes]() { return aBytes; },
        [aSrc, aBytes](Maybe<ProfileBufferEntryWriter>& aMaybeEntryWriter) {
          if (aMaybeEntryWriter.isNothing()) {
            return ProfileBufferBlockIndex{};
          }
          aMaybeEntryWriter->WriteBytes(aSrc, aBytes);
          return aMaybeEntryWriter->CurrentBlockIndex();
        });
  }

  // Add a new single entry with *all* given object (using a Serializer for
  // each), return block index.
  template <typename... Ts>
  ProfileBufferBlockIndex PutObjects(const Ts&... aTs) {
    static_assert(sizeof...(Ts) > 0,
                  "PutObjects must be given at least one object.");
    return ReserveAndPut(
        [&]() { return ProfileBufferEntryWriter::SumBytes(aTs...); },
        [&](Maybe<ProfileBufferEntryWriter>& aMaybeEntryWriter) {
          if (aMaybeEntryWriter.isNothing()) {
            return ProfileBufferBlockIndex{};
          }
          aMaybeEntryWriter->WriteObjects(aTs...);
          return aMaybeEntryWriter->CurrentBlockIndex();
        });
  }

  // Add a new entry copied from the given object, return block index.
  template <typename T>
  ProfileBufferBlockIndex PutObject(const T& aOb) {
    return PutObjects(aOb);
  }

  // Get *all* chunks related to this buffer, including extant chunks in its
  // ChunkManager, and yet-unused new/recycled chunks.
  // We don't expect this buffer to be used again, though it's still possible
  // and will allocate the first buffer when needed.
  [[nodiscard]] UniquePtr<ProfileBufferChunk> GetAllChunks() {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    if (MOZ_UNLIKELY(!mChunkManager)) {
      // Out-of-session.
      return nullptr;
    }
    UniquePtr<ProfileBufferChunk> chunks =
        mChunkManager->GetExtantReleasedChunks();
    Unused << HandleRequestedChunk_IsPending(lock);
    if (MOZ_LIKELY(!!mCurrentChunk)) {
      mCurrentChunk->MarkDone();
      chunks =
          ProfileBufferChunk::Join(std::move(chunks), std::move(mCurrentChunk));
    }
    chunks =
        ProfileBufferChunk::Join(std::move(chunks), std::move(mNextChunks));
    mChunkManager->ForgetUnreleasedChunks();
    mRangeStart = mRangeEnd = mNextChunkRangeStart;
    return chunks;
  }

  // True if the given index points inside the current chunk (up to the last
  // written byte).
  // This could be used to check if an index written now would have a good
  // chance of referring to a previous block that has not been destroyed yet.
  // But use with extreme care: This information may become incorrect right
  // after this function returns, because new writes could start a new chunk.
  [[nodiscard]] bool IsIndexInCurrentChunk(ProfileBufferIndex aIndex) const {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    if (MOZ_UNLIKELY(!mChunkManager || !mCurrentChunk)) {
      // Out-of-session, or no current chunk.
      return false;
    }
    return (mCurrentChunk->RangeStart() <= aIndex) &&
           (aIndex < (mCurrentChunk->RangeStart() +
                      mCurrentChunk->OffsetPastLastBlock()));
  }

  class Reader;

  // Class that can iterate through blocks and provide
  // `ProfileBufferEntryReader`s.
  // Created through `Reader`, lives within a lock guard lifetime.
  class BlockIterator {
   public:
#ifdef DEBUG
    ~BlockIterator() {
      // No BlockIterator should live outside of a mutexed call.
      mBuffer->mMutex.AssertCurrentThreadOwns();
    }
#endif  // DEBUG

    // Comparison with other iterator, mostly used in range-for loops.
    [[nodiscard]] bool operator==(const BlockIterator& aRhs) const {
      MOZ_ASSERT(mBuffer == aRhs.mBuffer);
      return mCurrentBlockIndex == aRhs.mCurrentBlockIndex;
    }
    [[nodiscard]] bool operator!=(const BlockIterator& aRhs) const {
      MOZ_ASSERT(mBuffer == aRhs.mBuffer);
      return mCurrentBlockIndex != aRhs.mCurrentBlockIndex;
    }

    // Advance to next BlockIterator.
    BlockIterator& operator++() {
      mBuffer->mMutex.AssertCurrentThreadOwns();
      mCurrentBlockIndex =
          ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
              mNextBlockPointer.GlobalRangePosition());
      mCurrentEntry =
          mNextBlockPointer.EntryReader(mNextBlockPointer.ReadEntrySize());
      return *this;
    }

    // Dereferencing creates a `ProfileBufferEntryReader` object for the entry
    // inside this block.
    // (Note: It would be possible to return a `const
    // ProfileBufferEntryReader&`, but not useful in practice, because in most
    // case the user will want to read, which is non-const.)
    [[nodiscard]] ProfileBufferEntryReader operator*() const {
      return mCurrentEntry;
    }

    // True if this iterator is just past the last entry.
    [[nodiscard]] bool IsAtEnd() const {
      return mCurrentEntry.RemainingBytes() == 0;
    }

    // Can be used as reference to come back to this entry with `GetEntryAt()`.
    [[nodiscard]] ProfileBufferBlockIndex CurrentBlockIndex() const {
      return mCurrentBlockIndex;
    }

    // Index past the end of this block, which is the start of the next block.
    [[nodiscard]] ProfileBufferBlockIndex NextBlockIndex() const {
      MOZ_ASSERT(!IsAtEnd());
      return ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
          mNextBlockPointer.GlobalRangePosition());
    }

    // Index of the first block in the whole buffer.
    [[nodiscard]] ProfileBufferBlockIndex BufferRangeStart() const {
      mBuffer->mMutex.AssertCurrentThreadOwns();
      return ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
          mBuffer->mRangeStart);
    }

    // Index past the last block in the whole buffer.
    [[nodiscard]] ProfileBufferBlockIndex BufferRangeEnd() const {
      mBuffer->mMutex.AssertCurrentThreadOwns();
      return ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
          mBuffer->mRangeEnd);
    }

   private:
    // Only a Reader can instantiate a BlockIterator.
    friend class Reader;

    BlockIterator(const ProfileChunkedBuffer& aBuffer,
                  const ProfileBufferChunk* aChunks0,
                  const ProfileBufferChunk* aChunks1,
                  ProfileBufferBlockIndex aBlockIndex)
        : mNextBlockPointer(aChunks0, aChunks1, aBlockIndex),
          mCurrentBlockIndex(
              ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
                  mNextBlockPointer.GlobalRangePosition())),
          mCurrentEntry(
              mNextBlockPointer.EntryReader(mNextBlockPointer.ReadEntrySize())),
          mBuffer(WrapNotNull(&aBuffer)) {
      // No BlockIterator should live outside of a mutexed call.
      mBuffer->mMutex.AssertCurrentThreadOwns();
    }

    profiler::detail::InChunkPointer mNextBlockPointer;

    ProfileBufferBlockIndex mCurrentBlockIndex;

    ProfileBufferEntryReader mCurrentEntry;

    // Using a non-null pointer instead of a reference, to allow copying.
    // This BlockIterator should only live inside one of the thread-safe
    // ProfileChunkedBuffer functions, for this reference to stay valid.
    NotNull<const ProfileChunkedBuffer*> mBuffer;
  };

  // Class that can create `BlockIterator`s (e.g., for range-for), or just
  // iterate through entries; lives within a lock guard lifetime.
  class MOZ_RAII Reader {
   public:
    Reader(const Reader&) = delete;
    Reader& operator=(const Reader&) = delete;
    Reader(Reader&&) = delete;
    Reader& operator=(Reader&&) = delete;

#ifdef DEBUG
    ~Reader() {
      // No Reader should live outside of a mutexed call.
      mBuffer.mMutex.AssertCurrentThreadOwns();
    }
#endif  // DEBUG

    // Index of the first block in the whole buffer.
    [[nodiscard]] ProfileBufferBlockIndex BufferRangeStart() const {
      mBuffer.mMutex.AssertCurrentThreadOwns();
      return ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
          mBuffer.mRangeStart);
    }

    // Index past the last block in the whole buffer.
    [[nodiscard]] ProfileBufferBlockIndex BufferRangeEnd() const {
      mBuffer.mMutex.AssertCurrentThreadOwns();
      return ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
          mBuffer.mRangeEnd);
    }

    // Iterators to the first and past-the-last blocks.
    // Compatible with range-for (see `ForEach` below as example).
    [[nodiscard]] BlockIterator begin() const {
      return BlockIterator(mBuffer, mChunks0, mChunks1, nullptr);
    }
    // Note that a `BlockIterator` at the `end()` should not be dereferenced, as
    // there is no actual block there!
    [[nodiscard]] BlockIterator end() const {
      return BlockIterator(mBuffer, nullptr, nullptr, nullptr);
    }

    // Get a `BlockIterator` at the given `ProfileBufferBlockIndex`, clamped to
    // the stored range. Note that a `BlockIterator` at the `end()` should not
    // be dereferenced, as there is no actual block there!
    [[nodiscard]] BlockIterator At(ProfileBufferBlockIndex aBlockIndex) const {
      if (aBlockIndex < BufferRangeStart()) {
        // Anything before the range (including null ProfileBufferBlockIndex) is
        // clamped at the beginning.
        return begin();
      }
      // Otherwise we at least expect the index to be valid (pointing exactly at
      // a live block, or just past the end.)
      return BlockIterator(mBuffer, mChunks0, mChunks1, aBlockIndex);
    }

    // Run `aCallback(ProfileBufferEntryReader&)` on each entry from first to
    // last. Callback should not store `ProfileBufferEntryReader`, as it may
    // become invalid after this thread-safe call.
    template <typename Callback>
    void ForEach(Callback&& aCallback) const {
      for (ProfileBufferEntryReader reader : *this) {
        aCallback(reader);
      }
    }

    // If this reader only points at one chunk with some data, this data will be
    // exposed as a single entry.
    [[nodiscard]] ProfileBufferEntryReader SingleChunkDataAsEntry() {
      const ProfileBufferChunk* onlyNonEmptyChunk = nullptr;
      for (const ProfileBufferChunk* chunkList : {mChunks0, mChunks1}) {
        for (const ProfileBufferChunk* chunk = chunkList; chunk;
             chunk = chunk->GetNext()) {
          if (chunk->OffsetFirstBlock() != chunk->OffsetPastLastBlock()) {
            if (onlyNonEmptyChunk) {
              // More than one non-empty chunk.
              return ProfileBufferEntryReader();
            }
            onlyNonEmptyChunk = chunk;
          }
        }
      }
      if (!onlyNonEmptyChunk) {
        // No non-empty chunks.
        return ProfileBufferEntryReader();
      }
      // Here, we have found one chunk that had some data.
      return ProfileBufferEntryReader(
          onlyNonEmptyChunk->BufferSpan().FromTo(
              onlyNonEmptyChunk->OffsetFirstBlock(),
              onlyNonEmptyChunk->OffsetPastLastBlock()),
          ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
              onlyNonEmptyChunk->RangeStart()),
          ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
              onlyNonEmptyChunk->RangeStart() +
              (onlyNonEmptyChunk->OffsetPastLastBlock() -
               onlyNonEmptyChunk->OffsetFirstBlock())));
    }

   private:
    friend class ProfileChunkedBuffer;

    explicit Reader(const ProfileChunkedBuffer& aBuffer,
                    const ProfileBufferChunk* aChunks0,
                    const ProfileBufferChunk* aChunks1)
        : mBuffer(aBuffer), mChunks0(aChunks0), mChunks1(aChunks1) {
      // No Reader should live outside of a mutexed call.
      mBuffer.mMutex.AssertCurrentThreadOwns();
    }

    // This Reader should only live inside one of the thread-safe
    // ProfileChunkedBuffer functions, for this reference to stay valid.
    const ProfileChunkedBuffer& mBuffer;
    const ProfileBufferChunk* mChunks0;
    const ProfileBufferChunk* mChunks1;
  };

  // In in-session, call `aCallback(ProfileChunkedBuffer::Reader&)` and return
  // true. Callback should not store `Reader`, because it may become invalid
  // after this call.
  // If out-of-session, return false (callback is not invoked).
  template <typename Callback>
  [[nodiscard]] auto Read(Callback&& aCallback) const {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    if (MOZ_UNLIKELY(!mChunkManager)) {
      // Out-of-session.
      return std::forward<Callback>(aCallback)(static_cast<Reader*>(nullptr));
    }
    return mChunkManager->PeekExtantReleasedChunks(
        [&](const ProfileBufferChunk* aOldestChunk) {
          Reader reader(*this, aOldestChunk, mCurrentChunk.get());
          return std::forward<Callback>(aCallback)(&reader);
        });
  }

  // Invoke `aCallback(ProfileBufferEntryReader& [, ProfileBufferBlockIndex])`
  // on each entry, it must read or at least skip everything. Either/both chunk
  // pointers may be null.
  template <typename Callback>
  static void ReadEach(const ProfileBufferChunk* aChunks0,
                       const ProfileBufferChunk* aChunks1,
                       Callback&& aCallback) {
    static_assert(std::is_invocable_v<Callback, ProfileBufferEntryReader&> ||
                      std::is_invocable_v<Callback, ProfileBufferEntryReader&,
                                          ProfileBufferBlockIndex>,
                  "ReadEach callback must take ProfileBufferEntryReader& and "
                  "optionally a ProfileBufferBlockIndex");
    profiler::detail::InChunkPointer p{aChunks0, aChunks1};
    while (!p.IsNull()) {
      // The position right before an entry size *is* a block index.
      const ProfileBufferBlockIndex blockIndex =
          ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
              p.GlobalRangePosition());
      Length entrySize = p.ReadEntrySize();
      if (entrySize == 0) {
        return;
      }
      ProfileBufferEntryReader entryReader = p.EntryReader(entrySize);
      if (entryReader.RemainingBytes() == 0) {
        return;
      }
      MOZ_ASSERT(entryReader.RemainingBytes() == entrySize);
      if constexpr (std::is_invocable_v<Callback, ProfileBufferEntryReader&,
                                        ProfileBufferBlockIndex>) {
        aCallback(entryReader, blockIndex);
      } else {
        Unused << blockIndex;
        aCallback(entryReader);
      }
      MOZ_ASSERT(entryReader.RemainingBytes() == 0);
    }
  }

  // Invoke `aCallback(ProfileBufferEntryReader& [, ProfileBufferBlockIndex])`
  // on each entry, it must read or at least skip everything.
  template <typename Callback>
  void ReadEach(Callback&& aCallback) const {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    if (MOZ_UNLIKELY(!mChunkManager)) {
      // Out-of-session.
      return;
    }
    mChunkManager->PeekExtantReleasedChunks(
        [&](const ProfileBufferChunk* aOldestChunk) {
          ReadEach(aOldestChunk, mCurrentChunk.get(),
                   std::forward<Callback>(aCallback));
        });
  }

  // Call `aCallback(Maybe<ProfileBufferEntryReader>&&)` on the entry at
  // the given ProfileBufferBlockIndex; The `Maybe` will be `Nothing` if
  // out-of-session, or if that entry doesn't exist anymore, or if we've reached
  // just past the last entry. Return whatever `aCallback` returns. Callback
  // should not store `ProfileBufferEntryReader`, because it may become invalid
  // after this call.
  // Either/both chunk pointers may be null.
  template <typename Callback>
  [[nodiscard]] static auto ReadAt(ProfileBufferBlockIndex aMinimumBlockIndex,
                                   const ProfileBufferChunk* aChunks0,
                                   const ProfileBufferChunk* aChunks1,
                                   Callback&& aCallback) {
    static_assert(
        std::is_invocable_v<Callback, Maybe<ProfileBufferEntryReader>&&>,
        "ReadAt callback must take a Maybe&&");
    Maybe<ProfileBufferEntryReader> maybeEntryReader;
    if (profiler::detail::InChunkPointer p{aChunks0, aChunks1}; !p.IsNull()) {
      // If the pointer position is before the given position, try to advance.
      if (p.GlobalRangePosition() >=
              aMinimumBlockIndex.ConvertToProfileBufferIndex() ||
          p.AdvanceToGlobalRangePosition(
              aMinimumBlockIndex.ConvertToProfileBufferIndex())) {
        MOZ_ASSERT(p.GlobalRangePosition() >=
                   aMinimumBlockIndex.ConvertToProfileBufferIndex());
        // Here we're pointing at the start of a block, try to read the entry
        // size. (Entries cannot be empty, so 0 means failure.)
        if (Length entrySize = p.ReadEntrySize(); entrySize != 0) {
          maybeEntryReader.emplace(p.EntryReader(entrySize));
          if (maybeEntryReader->RemainingBytes() == 0) {
            // An empty entry reader means there was no complete block at the
            // given index.
            maybeEntryReader.reset();
          } else {
            MOZ_ASSERT(maybeEntryReader->RemainingBytes() == entrySize);
          }
        }
      }
    }
#ifdef DEBUG
    auto assertAllRead = MakeScopeExit([&]() {
      MOZ_ASSERT(!maybeEntryReader || maybeEntryReader->RemainingBytes() == 0);
    });
#endif  // DEBUG
    return std::forward<Callback>(aCallback)(std::move(maybeEntryReader));
  }

  // Call `aCallback(Maybe<ProfileBufferEntryReader>&&)` on the entry at
  // the given ProfileBufferBlockIndex; The `Maybe` will be `Nothing` if
  // out-of-session, or if that entry doesn't exist anymore, or if we've reached
  // just past the last entry. Return whatever `aCallback` returns. Callback
  // should not store `ProfileBufferEntryReader`, because it may become invalid
  // after this call.
  template <typename Callback>
  [[nodiscard]] auto ReadAt(ProfileBufferBlockIndex aBlockIndex,
                            Callback&& aCallback) const {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    if (MOZ_UNLIKELY(!mChunkManager)) {
      // Out-of-session.
      return std::forward<Callback>(aCallback)(Nothing{});
    }
    return mChunkManager->PeekExtantReleasedChunks(
        [&](const ProfileBufferChunk* aOldestChunk) {
          return ReadAt(aBlockIndex, aOldestChunk, mCurrentChunk.get(),
                        std::forward<Callback>(aCallback));
        });
  }

  // Append the contents of another ProfileChunkedBuffer to this one.
  ProfileBufferBlockIndex AppendContents(const ProfileChunkedBuffer& aSrc) {
    ProfileBufferBlockIndex firstBlockIndex;
    // If we start failing, we'll stop writing.
    bool failed = false;
    aSrc.ReadEach([&](ProfileBufferEntryReader& aER) {
      if (failed) {
        return;
      }
      failed =
          !Put(aER.RemainingBytes(), [&](Maybe<ProfileBufferEntryWriter>& aEW) {
            if (aEW.isNothing()) {
              return false;
            }
            if (!firstBlockIndex) {
              firstBlockIndex = aEW->CurrentBlockIndex();
            }
            aEW->WriteFromReader(aER, aER.RemainingBytes());
            return true;
          });
    });
    return failed ? nullptr : firstBlockIndex;
  }

#ifdef DEBUG
  void Dump(std::FILE* aFile = stdout) const {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    fprintf(aFile,
            "ProfileChunkedBuffer[%p] State: range %u-%u pushed=%u cleared=%u "
            "(live=%u) failed-puts=%u bytes",
            thisunsigned(mRangeStart), unsigned(mRangeEnd),
            unsigned(mPushedBlockCount), unsigned(mClearedBlockCount),
            unsigned(mPushedBlockCount) - unsigned(mClearedBlockCount),
            unsigned(mFailedPutBytes));
    if (MOZ_UNLIKELY(!mChunkManager)) {
      fprintf(aFile, " - Out-of-session\n");
      return;
    }
    fprintf(aFile, " - chunks:\n");
    bool hasChunks = false;
    mChunkManager->PeekExtantReleasedChunks(
        [&](const ProfileBufferChunk* aOldestChunk) {
          for (const ProfileBufferChunk* chunk = aOldestChunk; chunk;
               chunk = chunk->GetNext()) {
            fprintf(aFile, "R ");
            chunk->Dump(aFile);
            hasChunks = true;
          }
        });
    if (mCurrentChunk) {
      fprintf(aFile, "C ");
      mCurrentChunk->Dump(aFile);
      hasChunks = true;
    }
    for (const ProfileBufferChunk* chunk = mNextChunks.get(); chunk;
         chunk = chunk->GetNext()) {
      fprintf(aFile, "N ");
      chunk->Dump(aFile);
      hasChunks = true;
    }
    switch (mRequestedChunkHolder->GetState()) {
      case RequestedChunkRefCountedHolder::State::Unused:
        fprintf(aFile, " - No request pending.\n");
        break;
      case RequestedChunkRefCountedHolder::State::Requested:
        fprintf(aFile, " - Request pending.\n");
        break;
      case RequestedChunkRefCountedHolder::State::Fulfilled:
        fprintf(aFile, " - Request fulfilled.\n");
        break;
    }
    if (!hasChunks) {
      fprintf(aFile, " No chunks.\n");
    }
  }
#endif  // DEBUG

 private:
  // Used to de/serialize a ProfileChunkedBuffer (e.g., containing a backtrace).
  friend ProfileBufferEntryWriter::Serializer<ProfileChunkedBuffer>;
  friend ProfileBufferEntryReader::Deserializer<ProfileChunkedBuffer>;
  friend ProfileBufferEntryWriter::Serializer<UniquePtr<ProfileChunkedBuffer>>;
  friend ProfileBufferEntryReader::Deserializer<
      UniquePtr<ProfileChunkedBuffer>>;

  [[nodiscard]] UniquePtr<ProfileBufferChunkManager> ResetChunkManager(
      const baseprofiler::detail::BaseProfilerMaybeAutoLock&) {
    UniquePtr<ProfileBufferChunkManager> chunkManager;
    if (mChunkManager) {
      mRequestedChunkHolder = nullptr;
      mChunkManager->ForgetUnreleasedChunks();
#ifdef DEBUG
      mChunkManager->DeregisteredFrom(this);
#endif
      mChunkManager = nullptr;
      chunkManager = std::move(mOwnedChunkManager);
      if (mCurrentChunk) {
        mCurrentChunk->MarkDone();
        mCurrentChunk = nullptr;
      }
      mNextChunks = nullptr;
      mNextChunkRangeStart = mRangeEnd;
      mRangeStart = mRangeEnd;
      mPushedBlockCount = 0;
      mClearedBlockCount = 0;
      mFailedPutBytes = 0;
    }
    return chunkManager;
  }

  void SetChunkManager(
      ProfileBufferChunkManager& aChunkManager,
      const baseprofiler::detail::BaseProfilerMaybeAutoLock& aLock) {
    MOZ_ASSERT(!mChunkManager);
    mChunkManager = &aChunkManager;
#ifdef DEBUG
    mChunkManager->RegisteredWith(this);
#endif

    mChunkManager->SetChunkDestroyedCallback(
        [this](const ProfileBufferChunk& aChunk) {
          for (;;) {
            ProfileBufferIndex rangeStart = mRangeStart;
            if (MOZ_LIKELY(rangeStart <= aChunk.RangeStart())) {
              if (MOZ_LIKELY(mRangeStart.compareExchange(
                      rangeStart,
                      aChunk.RangeStart() + aChunk.BufferBytes()))) {
                break;
              }
            }
          }
          mClearedBlockCount += aChunk.BlockCount();
        });

    // We start with one chunk right away, and request a following one now
    // so it should be available before the current chunk is full.
    SetAndInitializeCurrentChunk(mChunkManager->GetChunk(), aLock);
    mRequestedChunkHolder = MakeRefPtr<RequestedChunkRefCountedHolder>();
    RequestChunk(aLock);
  }

  [[nodiscard]] size_t SizeOfExcludingThis(
      MallocSizeOf aMallocSizeOf,
      const baseprofiler::detail::BaseProfilerMaybeAutoLock&) const {
    if (MOZ_UNLIKELY(!mChunkManager)) {
      // Out-of-session.
      return 0;
    }
    size_t size = mChunkManager->SizeOfIncludingThis(aMallocSizeOf);
    if (mCurrentChunk) {
      size += mCurrentChunk->SizeOfIncludingThis(aMallocSizeOf);
    }
    if (mNextChunks) {
      size += mNextChunks->SizeOfIncludingThis(aMallocSizeOf);
    }
    return size;
  }

  void InitializeCurrentChunk(
      const baseprofiler::detail::BaseProfilerMaybeAutoLock&) {
    MOZ_ASSERT(!!mCurrentChunk);
    mCurrentChunk->SetRangeStart(mNextChunkRangeStart);
    mNextChunkRangeStart += mCurrentChunk->BufferBytes();
    Unused << mCurrentChunk->ReserveInitialBlockAsTail(0);
  }

  void SetAndInitializeCurrentChunk(
      UniquePtr<ProfileBufferChunk>&& aChunk,
      const baseprofiler::detail::BaseProfilerMaybeAutoLock& aLock) {
    mCurrentChunk = std::move(aChunk);
    if (mCurrentChunk) {
      InitializeCurrentChunk(aLock);
    }
  }

  void RequestChunk(
      const baseprofiler::detail::BaseProfilerMaybeAutoLock& aLock) {
    if (HandleRequestedChunk_IsPending(aLock)) {
      // There is already a pending request, don't start a new one.
      return;
    }

    // Ensure the `RequestedChunkHolder` knows we're starting a request.
    mRequestedChunkHolder->StartRequest();

    // Request a chunk, the callback carries a `RefPtr` of the
    // `RequestedChunkHolder`, so it's guaranteed to live until it's invoked,
    // even if this `ProfileChunkedBuffer` changes its `ChunkManager` or is
    // destroyed.
    mChunkManager->RequestChunk(
        [requestedChunkHolder = RefPtr<RequestedChunkRefCountedHolder>(
             mRequestedChunkHolder)](UniquePtr<ProfileBufferChunk> aChunk) {
          requestedChunkHolder->AddRequestedChunk(std::move(aChunk));
        });
  }

  [[nodiscard]] bool HandleRequestedChunk_IsPending(
      const baseprofiler::detail::BaseProfilerMaybeAutoLock& aLock) {
    MOZ_ASSERT(!!mChunkManager);
    MOZ_ASSERT(!!mRequestedChunkHolder);

    if (mRequestedChunkHolder->GetState() ==
        RequestedChunkRefCountedHolder::State::Unused) {
      return false;
    }

    // A request is either in-flight or fulfilled.
    Maybe<UniquePtr<ProfileBufferChunk>> maybeChunk =
        mRequestedChunkHolder->GetChunkIfFulfilled();
    if (maybeChunk.isNothing()) {
      // Request is still pending.
      return true;
    }

    // Since we extracted the provided chunk, the holder should now be unused.
    MOZ_ASSERT(mRequestedChunkHolder->GetState() ==
               RequestedChunkRefCountedHolder::State::Unused);

    // Request has been fulfilled.
    UniquePtr<ProfileBufferChunk>& chunk = *maybeChunk;
    if (chunk) {
      // Try to use as current chunk if needed.
      if (!mCurrentChunk) {
        SetAndInitializeCurrentChunk(std::move(chunk), aLock);
        // We've just received a chunk and made it current, request a next chunk
        // for later.
        MOZ_ASSERT(!mNextChunks);
        RequestChunk(aLock);
        return true;
      }

      if (!mNextChunks) {
        mNextChunks = std::move(chunk);
      } else {
        mNextChunks->InsertNext(std::move(chunk));
      }
    }

    return false;
  }

  // Get a pointer to the next chunk available
  [[nodiscard]] ProfileBufferChunk* GetOrCreateCurrentChunk(
      const baseprofiler::detail::BaseProfilerMaybeAutoLock& aLock) {
    ProfileBufferChunk* current = mCurrentChunk.get();
    if (MOZ_UNLIKELY(!current)) {
      // No current chunk ready.
      MOZ_ASSERT(!mNextChunks,
                 "There shouldn't be next chunks when there is no current one");
      // See if a request has recently been fulfilled, ignore pending status.
      Unused << HandleRequestedChunk_IsPending(aLock);
      current = mCurrentChunk.get();
      if (MOZ_UNLIKELY(!current)) {
        // There was no pending chunk, try to get one right now.
        // This may still fail, but we can't do anything else about it, the
        // caller must handle the nullptr case.
        // Attempt a request for later.
        SetAndInitializeCurrentChunk(mChunkManager->GetChunk(), aLock);
        current = mCurrentChunk.get();
      }
    }
    return current;
  }

  // Get a pointer to the next chunk available
  [[nodiscard]] ProfileBufferChunk* GetOrCreateNextChunk(
      const baseprofiler::detail::BaseProfilerMaybeAutoLock& aLock) {
    MOZ_ASSERT(!!mCurrentChunk,
               "Why ask for a next chunk when there isn't even a current one?");
    ProfileBufferChunk* next = mNextChunks.get();
    if (MOZ_UNLIKELY(!next)) {
      // No next chunk ready, see if a request has recently been fulfilled,
      // ignore pending status.
      Unused << HandleRequestedChunk_IsPending(aLock);
      next = mNextChunks.get();
      if (MOZ_UNLIKELY(!next)) {
        // There was no pending chunk, try to get one right now.
        mNextChunks = mChunkManager->GetChunk();
        next = mNextChunks.get();
        // This may still fail, but we can't do anything else about it, the
        // caller must handle the nullptr case.
        if (MOZ_UNLIKELY(!next)) {
          // Attempt a request for later.
          RequestChunk(aLock);
        }
      }
    }
    return next;
  }

  // Reserve a block of `aCallbackBlockBytes()` size, and invoke and return
  // `aCallback(Maybe<ProfileBufferEntryWriter>&)`. Note that this is the "raw"
  // version that doesn't write the entry size at the beginning of the block.
  // Note: `aCallbackBlockBytes` is a callback instead of a simple value, to
  // delay this potentially-expensive computation until after we're checked that
  // we're in-session; use `Put(Length, Callback)` below if you know the size
  // already.
  template <typename CallbackBlockBytes, typename Callback>
  auto ReserveAndPutRaw(CallbackBlockBytes&& aCallbackBlockBytes,
                        Callback&& aCallback,
                        baseprofiler::detail::BaseProfilerMaybeAutoLock& aLock,
                        uint64_t aBlockCount = 1) {
    // The entry writer that will point into one or two chunks to write
    // into, empty by default (failure).
    Maybe<ProfileBufferEntryWriter> maybeEntryWriter;

    // The current chunk will be filled if we need to write more than its
    // remaining space.
    bool currentChunkFilled = false;

    // If the current chunk gets filled, we may or may not initialize the next
    // chunk!
    bool nextChunkInitialized = false;

    if (MOZ_LIKELY(mChunkManager)) {
      // In-session.

      const Length blockBytes =
          std::forward<CallbackBlockBytes>(aCallbackBlockBytes)();

      if (ProfileBufferChunk* current = GetOrCreateCurrentChunk(aLock);
          MOZ_LIKELY(current)) {
        if (blockBytes <= current->RemainingBytes()) {
          // Block fits in current chunk with only one span.
          currentChunkFilled = blockBytes == current->RemainingBytes();
          const auto [mem0, blockIndex] = current->ReserveBlock(blockBytes);
          MOZ_ASSERT(mem0.LengthBytes() == blockBytes);
          maybeEntryWriter.emplace(
              mem0, blockIndex,
              ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
                  blockIndex.ConvertToProfileBufferIndex() + blockBytes));
          MOZ_ASSERT(maybeEntryWriter->RemainingBytes() == blockBytes);
          mRangeEnd += blockBytes;
          mPushedBlockCount += aBlockCount;
        } else if (blockBytes >= current->BufferBytes()) {
          // Currently only two buffer chunks are held at a time and it is not
          // possible to write an object that takes up more space than this. In
          // this scenario, silently discard this block of data if it is unable
          // to fit into the two reserved profiler chunks.
          mFailedPutBytes += blockBytes;
        } else {
          // Block doesn't fit fully in current chunk, it needs to overflow into
          // the next one.
          // Whether or not we can write this entry, the current chunk is now
          // considered full, so it will be released. (Otherwise we could refuse
          // this entry, but later accept a smaller entry into this chunk, which
          // would be somewhat inconsistent.)
          currentChunkFilled = true;
          // Make sure the next chunk is available (from a previous request),
          // otherwise create one on the spot.
          if (ProfileBufferChunk* next = GetOrCreateNextChunk(aLock);
              MOZ_LIKELY(next)) {
            // Here, we know we have a current and a next chunk.
            // Reserve head of block at the end of the current chunk.
            const auto [mem0, blockIndex] =
                current->ReserveBlock(current->RemainingBytes());
            MOZ_ASSERT(mem0.LengthBytes() < blockBytes);
            MOZ_ASSERT(current->RemainingBytes() == 0);
            // Set the next chunk range, and reserve the needed space for the
            // tail of the block.
            next->SetRangeStart(mNextChunkRangeStart);
            mNextChunkRangeStart += next->BufferBytes();
            const auto mem1 = next->ReserveInitialBlockAsTail(
                blockBytes - mem0.LengthBytes());
            MOZ_ASSERT(next->RemainingBytes() != 0);
            nextChunkInitialized = true;
            // Block is split in two spans.
            maybeEntryWriter.emplace(
                mem0, mem1, blockIndex,
                ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
                    blockIndex.ConvertToProfileBufferIndex() + blockBytes));
            MOZ_ASSERT(maybeEntryWriter->RemainingBytes() == blockBytes);
            mRangeEnd += blockBytes;
            mPushedBlockCount += aBlockCount;
          } else {
            // Cannot get a new chunk. Record put failure.
            mFailedPutBytes += blockBytes;
          }
        }
      } else {
        // Cannot get a current chunk. Record put failure.
        mFailedPutBytes += blockBytes;
      }
    }  // end of `if (MOZ_LIKELY(mChunkManager))`

    // Here, we either have a `Nothing` (failure), or a non-empty entry writer
    // pointing at the start of the block.

    // After we invoke the callback and return, we may need to handle the
    // current chunk being filled.
    auto handleFilledChunk = MakeScopeExit([&]() {
      // If the entry writer was not already empty, the callback *must* have
      // filled the full entry.
      MOZ_ASSERT(!maybeEntryWriter || maybeEntryWriter->RemainingBytes() == 0);

      if (currentChunkFilled) {
        // Extract current (now filled) chunk.
        UniquePtr<ProfileBufferChunk> filled = std::move(mCurrentChunk);

        if (mNextChunks) {
          // Cycle to the next chunk.
          mCurrentChunk =
              std::exchange(mNextChunks, mNextChunks->ReleaseNext());

          // Make sure it is initialized (it is now the current chunk).
          if (!nextChunkInitialized) {
            InitializeCurrentChunk(aLock);
          }
        }

        // And finally mark filled chunk done and release it.
        filled->MarkDone();
        mChunkManager->ReleaseChunk(std::move(filled));

        // Request another chunk if needed.
        // In most cases, here we should have one current chunk and no next
        // chunk, so we want to do a request so there hopefully will be a next
        // chunk available when the current one gets filled.
        // But we also for a request if we don't even have a current chunk (if
        // it's too late, it's ok because the next `ReserveAndPutRaw` wil just
        // allocate one on the spot.)
        // And if we already have a next chunk, there's no need for more now.
        if (!mCurrentChunk || !mNextChunks) {
          RequestChunk(aLock);
        }
      }
    });

    return std::forward<Callback>(aCallback)(maybeEntryWriter);
  }

  // Reserve a block of `aBlockBytes` size, and invoke and return
  // `aCallback(Maybe<ProfileBufferEntryWriter>&)`. Note that this is the "raw"
  // version that doesn't write the entry size at the beginning of the block.
  template <typename Callback>
  auto ReserveAndPutRaw(Length aBlockBytes, Callback&& aCallback,
                        uint64_t aBlockCount) {
    baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex);
    return ReserveAndPutRaw([aBlockBytes]() { return aBlockBytes; },
                            std::forward<Callback>(aCallback), lock,
                            aBlockCount);
  }

  // Mutex guarding the following members.
  mutable baseprofiler::detail::BaseProfilerMaybeMutex mMutex;

  // Pointer to the current Chunk Manager (or null when out-of-session.)
  // It may be owned locally (see below) or externally.
  ProfileBufferChunkManager* mChunkManager = nullptr;

  // Only non-null when we own the current Chunk Manager.
  UniquePtr<ProfileBufferChunkManager> mOwnedChunkManager;

  UniquePtr<ProfileBufferChunk> mCurrentChunk;

  UniquePtr<ProfileBufferChunk> mNextChunks;

  // Class used to transfer requested chunks from a `ChunkManager` to a
  // `ProfileChunkedBuffer`.
  // It needs to be ref-counted because the request may be fulfilled
  // asynchronously, and either side may be destroyed during the request.
  // It cannot use the `ProfileChunkedBuffer` mutex, because that buffer and its
  // mutex could be destroyed during the request.
  class RequestedChunkRefCountedHolder {
   public:
    enum class State { Unused, Requested, Fulfilled };

    // Get the current state. Note that it may change after the function
    // returns, so it should be used carefully, e.g., `ProfileChunkedBuffer` can
    // see if a request is pending or fulfilled, to avoid starting another
    // request.
    [[nodiscard]] State GetState() const {
      baseprofiler::detail::BaseProfilerAutoLock lock(mRequestMutex);
      return mState;
    }

    // Must be called by `ProfileChunkedBuffer` when it requests a chunk.
    // There cannot be more than one request in-flight.
    void StartRequest() {
      baseprofiler::detail::BaseProfilerAutoLock lock(mRequestMutex);
      MOZ_ASSERT(mState == State::Unused, "Already requested or fulfilled");
      mState = State::Requested;
    }

    // Must be called by the `ChunkManager` with a chunk.
    // If the `ChunkManager` cannot provide a chunk (because of memory limits,
    // or it gets destroyed), it must call this anyway with a nullptr.
    void AddRequestedChunk(UniquePtr<ProfileBufferChunk>&& aChunk) {
      baseprofiler::detail::BaseProfilerAutoLock lock(mRequestMutex);
      MOZ_ASSERT(mState == State::Requested);
      mState = State::Fulfilled;
      mRequestedChunk = std::move(aChunk);
    }

    // The `ProfileChunkedBuffer` can try to extract the provided chunk after a
    // request:
    // - Nothing -> Request is not fulfilled yet.
    // - Some(nullptr) -> The `ChunkManager` was not able to provide a chunk.
    // - Some(chunk) -> Requested chunk.
    [[nodiscard]] Maybe<UniquePtr<ProfileBufferChunk>> GetChunkIfFulfilled() {
      Maybe<UniquePtr<ProfileBufferChunk>> maybeChunk;
      baseprofiler::detail::BaseProfilerAutoLock lock(mRequestMutex);
      MOZ_ASSERT(mState == State::Requested || mState == State::Fulfilled);
      if (mState == State::Fulfilled) {
        mState = State::Unused;
        maybeChunk.emplace(std::move(mRequestedChunk));
      }
      return maybeChunk;
    }

    // Ref-counting implementation. Hand-rolled, because mozilla::RefCounted
    // logs AddRefs and Releases in xpcom, but this object could be AddRef'd
    // by the Base Profiler before xpcom starts, then Release'd by the Gecko
    // Profiler in xpcom, leading to apparent negative leaks.

    void AddRef() {
      baseprofiler::detail::BaseProfilerAutoLock lock(mRequestMutex);
      ++mRefCount;
    }

    void Release() {
      {
        baseprofiler::detail::BaseProfilerAutoLock lock(mRequestMutex);
        if (--mRefCount > 0) {
          return;
        }
      }
      delete this;
    }

   private:
    ~RequestedChunkRefCountedHolder() = default;

    // Mutex guarding the following members.
    mutable baseprofiler::detail::BaseProfilerMutex mRequestMutex;
    int mRefCount = 0;
    State mState = State::Unused;
    UniquePtr<ProfileBufferChunk> mRequestedChunk;
  };

  // Requested-chunk holder, kept alive when in-session, but may also live
  // longer if a request is in-flight.
  RefPtr<RequestedChunkRefCountedHolder> mRequestedChunkHolder;

  // Range start of the next chunk to become current. Starting at 1 because
  // 0 is a reserved index similar to nullptr.
  ProfileBufferIndex mNextChunkRangeStart = 1;

  // Index to the first block.
  // Atomic because it may be increased when a Chunk is destroyed, and the
  // callback may be invoked from anywhere, including from inside one of our
  // locked section, so we cannot protect it with a mutex.
  Atomic<ProfileBufferIndex, MemoryOrdering::ReleaseAcquire> mRangeStart{1};

  // Index past the last block. Equals mRangeStart if empty.
  ProfileBufferIndex mRangeEnd = 1;

  // Number of blocks that have been pushed into this buffer.
  uint64_t mPushedBlockCount = 0;

  // Number of blocks that have been removed from this buffer.
  // Note: Live entries = pushed - cleared.
  // Atomic because it may be updated when a Chunk is destroyed, and the
  // callback may be invoked from anywhere, including from inside one of our
  // locked section, so we cannot protect it with a mutex.
  Atomic<uint64_t, MemoryOrdering::ReleaseAcquire> mClearedBlockCount{0};

  // Number of bytes that could not be put into this buffer.
  uint64_t mFailedPutBytes = 0;
};

// ----------------------------------------------------------------------------
// ProfileChunkedBuffer serialization

// A ProfileChunkedBuffer can hide another one!
// This will be used to store marker backtraces; They can be read back into a
// UniquePtr<ProfileChunkedBuffer>.
// Format: len (ULEB128) | start | end | buffer (len bytes) | pushed | cleared
// len==0 marks an out-of-session buffer, or empty buffer.
template <>
struct ProfileBufferEntryWriter::Serializer<ProfileChunkedBuffer> {
  static Length Bytes(const ProfileChunkedBuffer& aBuffer) {
    return aBuffer.Read([&](ProfileChunkedBuffer::Reader* aReader) {
      if (!aReader) {
        // Out-of-session, we only need 1 byte to store a length of 0.
        return ULEB128Size<Length>(0);
      }
      ProfileBufferEntryReader reader = aReader->SingleChunkDataAsEntry();
      const ProfileBufferIndex start =
          reader.CurrentBlockIndex().ConvertToProfileBufferIndex();
      const ProfileBufferIndex end =
          reader.NextBlockIndex().ConvertToProfileBufferIndex();
      MOZ_ASSERT(end - start <= std::numeric_limits<Length>::max());
      const Length len = static_cast<Length>(end - start);
      if (len == 0) {
        // In-session but empty, also store a length of 0.
        return ULEB128Size<Length>(0);
      }
      // In-session.
      return static_cast<Length>(ULEB128Size(len) + sizeof(start) + len +
                                 sizeof(aBuffer.mPushedBlockCount) +
                                 sizeof(aBuffer.mClearedBlockCount));
    });
  }

  static void Write(ProfileBufferEntryWriter& aEW,
                    const ProfileChunkedBuffer& aBuffer) {
    aBuffer.Read([&](ProfileChunkedBuffer::Reader* aReader) {
      if (!aReader) {
        // Out-of-session, only store a length of 0.
        aEW.WriteULEB128<Length>(0);
        return;
      }
      ProfileBufferEntryReader reader = aReader->SingleChunkDataAsEntry();
      const ProfileBufferIndex start =
          reader.CurrentBlockIndex().ConvertToProfileBufferIndex();
      const ProfileBufferIndex end =
          reader.NextBlockIndex().ConvertToProfileBufferIndex();
      MOZ_ASSERT(end - start <= std::numeric_limits<Length>::max());
      const Length len = static_cast<Length>(end - start);
      MOZ_ASSERT(len <= aEW.RemainingBytes());
      if (len == 0) {
        // In-session but empty, only store a length of 0.
        aEW.WriteULEB128<Length>(0);
        return;
      }
      // In-session.
      // Store buffer length, and start index.
      aEW.WriteULEB128(len);
      aEW.WriteObject(start);
      // Write all the bytes.
      aEW.WriteFromReader(reader, reader.RemainingBytes());
      // And write stats.
      aEW.WriteObject(static_cast<uint64_t>(aBuffer.mPushedBlockCount));
      aEW.WriteObject(static_cast<uint64_t>(aBuffer.mClearedBlockCount));
      // Note: Failed pushes are not important to serialize.
    });
  }
};

// A serialized ProfileChunkedBuffer can be read into an empty buffer (either
// out-of-session, or in-session with enough room).
template <>
struct ProfileBufferEntryReader::Deserializer<ProfileChunkedBuffer> {
  static void ReadInto(ProfileBufferEntryReader& aER,
                       ProfileChunkedBuffer& aBuffer) {
    // Expect an empty buffer, as we're going to overwrite it.
    MOZ_ASSERT(aBuffer.GetState().mRangeStart == aBuffer.GetState().mRangeEnd);
    // Read the stored buffer length.
    const auto len = aER.ReadULEB128<ProfileChunkedBuffer::Length>();
    if (len == 0) {
      // 0-length means an "uninteresting" buffer, just return now.
      return;
    }
    // We have a non-empty buffer to read.

    // Read start and end indices.
    const auto start = aER.ReadObject<ProfileBufferIndex>();
    aBuffer.mRangeStart = start;
    // For now, set the end to be the start (the buffer is still empty). It will
    // be updated in `ReserveAndPutRaw()` below.
    aBuffer.mRangeEnd = start;

    if (aBuffer.IsInSession()) {
      // Output buffer is in-session (i.e., it already has a memory buffer
      // attached). Make sure the caller allocated enough space.
      MOZ_RELEASE_ASSERT(aBuffer.BufferLength().value() >= len);
    } else {
      // Output buffer is out-of-session, set a new chunk manager that will
      // provide a single chunk of just the right size.
      aBuffer.SetChunkManager(MakeUnique<ProfileBufferChunkManagerSingle>(len));
      MOZ_ASSERT(aBuffer.BufferLength().value() >= len);
    }

    // Copy bytes into the buffer.
    aBuffer.ReserveAndPutRaw(
        len,
        [&](Maybe<ProfileBufferEntryWriter>& aEW) {
          MOZ_RELEASE_ASSERT(aEW.isSome());
          aEW->WriteFromReader(aER, len);
        },
        0);
    // Finally copy stats.
    aBuffer.mPushedBlockCount = aER.ReadObject<uint64_t>();
    aBuffer.mClearedBlockCount = aER.ReadObject<uint64_t>();
    // Failed puts are not important to keep.
    aBuffer.mFailedPutBytes = 0;
  }

  // We cannot output a ProfileChunkedBuffer object (not copyable), use
  // `ReadInto()` or `aER.ReadObject<UniquePtr<BlocksRinbBuffer>>()` instead.
  static ProfileChunkedBuffer Read(ProfileBufferEntryReader& aER) = delete;
};

// A ProfileChunkedBuffer is usually refererenced through a UniquePtr, for
// convenience we support (de)serializing that UniquePtr directly.
// This is compatible with the non-UniquePtr serialization above, with a null
// pointer being treated like an out-of-session or empty buffer; and any of
// these would be deserialized into a null pointer.
template <>
struct ProfileBufferEntryWriter::Serializer<UniquePtr<ProfileChunkedBuffer>> {
  static Length Bytes(const UniquePtr<ProfileChunkedBuffer>& aBufferUPtr) {
    if (!aBufferUPtr) {
      // Null pointer, treat it like an empty buffer, i.e., write length of 0.
      return ULEB128Size<Length>(0);
    }
    // Otherwise write the pointed-at ProfileChunkedBuffer (which could be
    // out-of-session or empty.)
    return SumBytes(*aBufferUPtr);
  }

  static void Write(ProfileBufferEntryWriter& aEW,
                    const UniquePtr<ProfileChunkedBuffer>& aBufferUPtr) {
    if (!aBufferUPtr) {
      // Null pointer, treat it like an empty buffer, i.e., write length of 0.
      aEW.WriteULEB128<Length>(0);
      return;
    }
    // Otherwise write the pointed-at ProfileChunkedBuffer (which could be
    // out-of-session or empty.)
    aEW.WriteObject(*aBufferUPtr);
  }
};

// Serialization of a raw pointer to ProfileChunkedBuffer.
// Use Deserializer<UniquePtr<ProfileChunkedBuffer>> to read it back.
template <>
struct ProfileBufferEntryWriter::Serializer<ProfileChunkedBuffer*> {
  static Length Bytes(ProfileChunkedBuffer* aBufferUPtr) {
    if (!aBufferUPtr) {
      // Null pointer, treat it like an empty buffer, i.e., write length of 0.
      return ULEB128Size<Length>(0);
    }
    // Otherwise write the pointed-at ProfileChunkedBuffer (which could be
    // out-of-session or empty.)
    return SumBytes(*aBufferUPtr);
  }

  static void Write(ProfileBufferEntryWriter& aEW,
                    ProfileChunkedBuffer* aBufferUPtr) {
    if (!aBufferUPtr) {
      // Null pointer, treat it like an empty buffer, i.e., write length of 0.
      aEW.WriteULEB128<Length>(0);
      return;
    }
    // Otherwise write the pointed-at ProfileChunkedBuffer (which could be
    // out-of-session or empty.)
    aEW.WriteObject(*aBufferUPtr);
  }
};

template <>
struct ProfileBufferEntryReader::Deserializer<UniquePtr<ProfileChunkedBuffer>> {
  static void ReadInto(ProfileBufferEntryReader& aER,
                       UniquePtr<ProfileChunkedBuffer>& aBuffer) {
    aBuffer = Read(aER);
  }

  static UniquePtr<ProfileChunkedBuffer> Read(ProfileBufferEntryReader& aER) {
    UniquePtr<ProfileChunkedBuffer> bufferUPtr;
    // Keep a copy of the reader before reading the length, so we can restart
    // from here below.
    ProfileBufferEntryReader readerBeforeLen = aER;
    // Read the stored buffer length.
    const auto len = aER.ReadULEB128<ProfileChunkedBuffer::Length>();
    if (len == 0) {
      // 0-length means an "uninteresting" buffer, just return nullptr.
      return bufferUPtr;
    }
    // We have a non-empty buffer.
    // allocate an empty ProfileChunkedBuffer without mutex.
    bufferUPtr = MakeUnique<ProfileChunkedBuffer>(
        ProfileChunkedBuffer::ThreadSafety::WithoutMutex);
    // Rewind the reader before the length and deserialize the contents, using
    // the non-UniquePtr Deserializer.
    aER = readerBeforeLen;
    aER.ReadIntoObject(*bufferUPtr);
    return bufferUPtr;
  }
};

}  // namespace mozilla

#endif  // ProfileChunkedBuffer_h

Messung V0.5
C=90 H=94 G=91

¤ Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.0.61Bemerkung:  (vorverarbeitet)  ¤

*Bot Zugriff






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.