/* -*- 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 BlocksRingBuffer_h #define BlocksRingBuffer_h #include "mozilla/BaseProfilerDetail.h" #include "mozilla/ModuloBuffer.h" #include "mozilla/ProfileBufferIndex.h" #include "mozilla/ScopeExit.h" #include #include #include #include namespace mozilla { // Thread-safe Ring buffer that can store blocks of different sizes during // defined sessions. // Each *block* contains an *entry* and the entry size: // [ entry_size | entry ] [ entry_size | entry ] ... // *In-session* is a period of time during which `BlocksRingBuffer` allows // reading and writing. *Out-of-session*, the `BlocksRingBuffer` 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 ModuloBuffer::Iterator APIs and a few entry- // specific APIs. E.g.: // ``` // BlockRingsBuffer brb(PowerOfTwo(1024)); // brb.ReserveAndPut([]() { return sizeof(123); }, // [&](ProfileBufferEntryWriter& 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 // the BlocksRingBuffer and therefore should not live longer. // Different type of objects may be serialized into an entry, see `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.: // ``` // brb.Read([](BlocksRingBuffer::Reader aR) {} // for (ProfileBufferEntryReader aER : aR) { // /* Use ProfileBufferEntryReader functions to read serialized objects. */ // int n = aER.ReadObject(); // } // }); // ``` // Different type of objects may be deserialized from an entry, see // `Deserializer` for more information. // // The caller may retrieve the `ProfileBufferBlockIndex` corresponding to an // entry (`ProfileBufferBlockIndex` is an opaque type preventing the user from // modifying it). That index may later be used to get back to that particular // entry if it still exists. class BlocksRingBuffer { public: // Using ModuloBuffer as underlying circular byte buffer. using Buffer = ModuloBuffer; using Byte = Buffer::Byte; // Length type for total buffer (as PowerOfTwo) and each entry. using Length = uint32_t; enum class ThreadSafety { WithoutMutex, WithMutex }; // Default constructor starts out-of-session (nothing to read or write). explicit BlocksRingBuffer(ThreadSafety aThreadSafety) : mMutex(aThreadSafety != ThreadSafety::WithoutMutex) {} // Create a buffer of the given length. explicit BlocksRingBuffer(ThreadSafety aThreadSafety, PowerOfTwo aLength) : mMutex(aThreadSafety != ThreadSafety::WithoutMutex), mMaybeUnderlyingBuffer(Some(UnderlyingBuffer(aLength))) {} // Take ownership of an existing buffer. BlocksRingBuffer(ThreadSafety aThreadSafety, UniquePtr aExistingBuffer, PowerOfTwo aLength) : mMutex(aThreadSafety != ThreadSafety::WithoutMutex), mMaybeUnderlyingBuffer( Some(UnderlyingBuffer(std::move(aExistingBuffer), aLength))) {} // Use an externally-owned buffer. BlocksRingBuffer(ThreadSafety aThreadSafety, Buffer::Byte* aExternalBuffer, PowerOfTwo aLength) : mMutex(aThreadSafety != ThreadSafety::WithoutMutex), mMaybeUnderlyingBuffer( Some(UnderlyingBuffer(aExternalBuffer, aLength))) {} // Destructor doesn't need to do anything special. (Clearing entries would // only update indices and stats, which won't be accessible after the object // is destroyed anyway.) ~BlocksRingBuffer() = default; // Remove underlying buffer, if any. void Reset() { baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex); ResetUnderlyingBuffer(); } // Create a buffer of the given length. void Set(PowerOfTwo aLength) { baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex); ResetUnderlyingBuffer(); mMaybeUnderlyingBuffer.emplace(aLength); } // Take ownership of an existing buffer. void Set(UniquePtr aExistingBuffer, PowerOfTwo aLength) { baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex); ResetUnderlyingBuffer(); mMaybeUnderlyingBuffer.emplace(std::move(aExistingBuffer), aLength); } // Use an externally-owned buffer. void Set(Buffer::Byte* aExternalBuffer, PowerOfTwo aLength) { baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex); ResetUnderlyingBuffer(); mMaybeUnderlyingBuffer.emplace(aExternalBuffer, aLength); } // This cannot change during the lifetime of this buffer, so there's no need // to lock. bool IsThreadSafe() const { return mMutex.IsActivated(); } [[nodiscard]] bool IsInSession() const { baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex); return !!mMaybeUnderlyingBuffer; } // 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 auto LockAndRun(Callback&& aCallback) const { baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex); return std::forward(aCallback)(); } // Buffer length in bytes. Maybe> BufferLength() const { baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex); return mMaybeUnderlyingBuffer.map([](const UnderlyingBuffer& aBuffer) { return aBuffer.mBuffer.BufferLength(); }); ; } // Size of external resources. size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { if (!mMaybeUnderlyingBuffer) { return 0; } return mMaybeUnderlyingBuffer->mBuffer.SizeOfExcludingThis(aMallocSizeOf); } size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); } // Snapshot of the buffer state. struct State { // Index to the first block. ProfileBufferBlockIndex mRangeStart; // Index past the last block. Equals mRangeStart if empty. ProfileBufferBlockIndex mRangeEnd; // 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; }; // 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. State GetState() const { baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex); return { mFirstReadIndex, mNextWriteIndex, mMaybeUnderlyingBuffer ? mMaybeUnderlyingBuffer->mPushedBlockCount : 0, mMaybeUnderlyingBuffer ? mMaybeUnderlyingBuffer->mClearedBlockCount : 0}; } 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. mRing->mMutex.AssertCurrentThreadOwns(); } #endif // DEBUG // Comparison with other iterator, mostly used in range-for loops. bool operator==(const BlockIterator aRhs) const { MOZ_ASSERT(mRing == aRhs.mRing); return mBlockIndex == aRhs.mBlockIndex; } bool operator!=(const BlockIterator aRhs) const { MOZ_ASSERT(mRing == aRhs.mRing); return mBlockIndex != aRhs.mBlockIndex; } // Advance to next BlockIterator. BlockIterator& operator++() { mBlockIndex = NextBlockIndex(); return *this; } // Dereferencing creates a `ProfileBufferEntryReader` for the entry inside // this block. ProfileBufferEntryReader operator*() const { return mRing->ReaderInBlockAt(mBlockIndex); } // True if this iterator is just past the last entry. bool IsAtEnd() const { MOZ_ASSERT(mBlockIndex <= BufferRangeEnd()); return mBlockIndex == BufferRangeEnd(); } // Can be used as reference to come back to this entry with `ReadAt()`. ProfileBufferBlockIndex CurrentBlockIndex() const { return mBlockIndex; } // Index past the end of this block, which is the start of the next block. ProfileBufferBlockIndex NextBlockIndex() const { MOZ_ASSERT(!IsAtEnd()); const Length entrySize = mRing->ReaderInBlockAt(mBlockIndex).RemainingBytes(); return ProfileBufferBlockIndex::CreateFromProfileBufferIndex( mBlockIndex.ConvertToProfileBufferIndex() + ULEB128Size(entrySize) + entrySize); } // Index of the first block in the whole buffer. ProfileBufferBlockIndex BufferRangeStart() const { return mRing->mFirstReadIndex; } // Index past the last block in the whole buffer. ProfileBufferBlockIndex BufferRangeEnd() const { return mRing->mNextWriteIndex; } private: // Only a Reader can instantiate a BlockIterator. friend class Reader; BlockIterator(const BlocksRingBuffer& aRing, ProfileBufferBlockIndex aBlockIndex) : mRing(WrapNotNull(&aRing)), mBlockIndex(aBlockIndex) { // No BlockIterator should live outside of a mutexed call. mRing->mMutex.AssertCurrentThreadOwns(); } // Using a non-null pointer instead of a reference, to allow copying. // This BlockIterator should only live inside one of the thread-safe // BlocksRingBuffer functions, for this reference to stay valid. NotNull mRing; ProfileBufferBlockIndex mBlockIndex; }; // 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. mRing.mMutex.AssertCurrentThreadOwns(); } #endif // DEBUG // Index of the first block in the whole buffer. ProfileBufferBlockIndex BufferRangeStart() const { return mRing.mFirstReadIndex; } // Index past the last block in the whole buffer. ProfileBufferBlockIndex BufferRangeEnd() const { return mRing.mNextWriteIndex; } // Iterators to the first and past-the-last blocks. // Compatible with range-for (see `ForEach` below as example). BlockIterator begin() const { return BlockIterator(mRing, BufferRangeStart()); } // Note that a `BlockIterator` at the `end()` should not be dereferenced, as // there is no actual block there! BlockIterator end() const { return BlockIterator(mRing, BufferRangeEnd()); } // 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! 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.) mRing.AssertBlockIndexIsValidOrEnd(aBlockIndex); return BlockIterator(mRing, 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 void ForEach(Callback&& aCallback) const { for (ProfileBufferEntryReader reader : *this) { aCallback(reader); } } private: friend class BlocksRingBuffer; explicit Reader(const BlocksRingBuffer& aRing) : mRing(aRing) { // No Reader should live outside of a mutexed call. mRing.mMutex.AssertCurrentThreadOwns(); } // This Reader should only live inside one of the thread-safe // BlocksRingBuffer functions, for this reference to stay valid. const BlocksRingBuffer& mRing; }; // Call `aCallback(BlocksRingBuffer::Reader*)` (nullptr when out-of-session), // and return whatever `aCallback` returns. Callback should not store // `Reader`, because it may become invalid after this call. template auto Read(Callback&& aCallback) const { { baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex); if (MOZ_LIKELY(mMaybeUnderlyingBuffer)) { Reader reader(*this); return std::forward(aCallback)(&reader); } } return std::forward(aCallback)(nullptr); } // Call `aCallback(ProfileBufferEntryReader&)` on each item. // Callback should not store `ProfileBufferEntryReader`, because it may become // invalid after this call. template void ReadEach(Callback&& aCallback) const { Read([&](Reader* aReader) { if (MOZ_LIKELY(aReader)) { aReader->ForEach(aCallback); } }); } // Call `aCallback(Maybe&&)` 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 auto ReadAt(ProfileBufferBlockIndex aBlockIndex, Callback&& aCallback) const { baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex); MOZ_ASSERT(aBlockIndex <= mNextWriteIndex); Maybe maybeEntryReader; if (MOZ_LIKELY(mMaybeUnderlyingBuffer) && aBlockIndex >= mFirstReadIndex && aBlockIndex < mNextWriteIndex) { AssertBlockIndexIsValid(aBlockIndex); maybeEntryReader.emplace(ReaderInBlockAt(aBlockIndex)); } return std::forward(aCallback)(std::move(maybeEntryReader)); } // Main function to write entries. // Reserve `aCallbackBytes()` bytes, call `aCallback()` with a pointer to an // on-stack temporary ProfileBufferEntryWriter (nullptr when out-of-session), // and return whatever `aCallback` returns. Callback should not store // `ProfileBufferEntryWriter`, because it may become invalid after this // thread-safe call. Note: `aCallbackBytes` 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 auto ReserveAndPut(CallbackBytes aCallbackBytes, Callback&& aCallback) { Maybe maybeEntryWriter; baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex); if (MOZ_LIKELY(mMaybeUnderlyingBuffer)) { const Length entryBytes = std::forward(aCallbackBytes)(); MOZ_RELEASE_ASSERT(entryBytes > 0); const Length bufferBytes = mMaybeUnderlyingBuffer->mBuffer.BufferLength().Value(); MOZ_RELEASE_ASSERT(entryBytes <= bufferBytes - ULEB128Size(entryBytes), "Entry would wrap and overwrite itself"); // Compute block size from the requested entry size. const Length blockBytes = ULEB128Size(entryBytes) + entryBytes; // We will put this new block at the end of the current buffer. const ProfileBufferIndex blockIndex = mNextWriteIndex.ConvertToProfileBufferIndex(); // Compute the end of this new block. const ProfileBufferIndex blockEnd = blockIndex + blockBytes; while (blockEnd > mFirstReadIndex.ConvertToProfileBufferIndex() + bufferBytes) { // About to trample on an old block. ProfileBufferEntryReader reader = ReaderInBlockAt(mFirstReadIndex); mMaybeUnderlyingBuffer->mClearedBlockCount += 1; // Move the buffer reading start past this cleared block. mFirstReadIndex = ProfileBufferBlockIndex::CreateFromProfileBufferIndex( mFirstReadIndex.ConvertToProfileBufferIndex() + ULEB128Size(reader.RemainingBytes()) + reader.RemainingBytes()); } // Store the new end of buffer. mNextWriteIndex = ProfileBufferBlockIndex::CreateFromProfileBufferIndex(blockEnd); mMaybeUnderlyingBuffer->mPushedBlockCount += 1; // Finally, let aCallback write into the entry. mMaybeUnderlyingBuffer->mBuffer.EntryWriterFromTo(maybeEntryWriter, blockIndex, blockEnd); MOZ_ASSERT(maybeEntryWriter.isSome(), "Non-empty entry should always create an EntryWriter"); maybeEntryWriter->WriteULEB128(entryBytes); MOZ_ASSERT(maybeEntryWriter->RemainingBytes() == entryBytes); } #ifdef DEBUG auto checkAllWritten = MakeScopeExit([&]() { MOZ_ASSERT(!maybeEntryWriter || maybeEntryWriter->RemainingBytes() == 0); }); #endif // DEBUG return std::forward(aCallback)(maybeEntryWriter); } // Add a new entry of known size, call `aCallback` with a pointer to a // temporary ProfileBufferEntryWriter (can be null when out-of-session), and // return whatever `aCallback` returns. Callback should not store the // `ProfileBufferEntryWriter`, as it may become invalid after this thread-safe // call. template auto Put(Length aBytes, Callback&& aCallback) { return ReserveAndPut([aBytes]() { return aBytes; }, std::forward(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; }, [&](Maybe& aEntryWriter) { if (MOZ_UNLIKELY(aEntryWriter.isNothing())) { // Out-of-session, return "empty" index. return ProfileBufferBlockIndex{}; } aEntryWriter->WriteBytes(aSrc, aBytes); return aEntryWriter->CurrentBlockIndex(); }); } // Add a new single entry with *all* given object (using a Serializer for // each), return block index. template 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& aEntryWriter) { if (MOZ_UNLIKELY(aEntryWriter.isNothing())) { // Out-of-session, return "empty" index. return ProfileBufferBlockIndex{}; } aEntryWriter->WriteObjects(aTs...); return aEntryWriter->CurrentBlockIndex(); }); } // Add a new entry copied from the given object, return block index. template ProfileBufferBlockIndex PutObject(const T& aOb) { return PutObjects(aOb); } // Append the contents of another BlocksRingBuffer to this one. ProfileBufferBlockIndex AppendContents(const BlocksRingBuffer& aSrc) { baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex); if (MOZ_UNLIKELY(!mMaybeUnderlyingBuffer)) { // We are out-of-session, could not append contents. return ProfileBufferBlockIndex{}; } baseprofiler::detail::BaseProfilerMaybeAutoLock srcLock(aSrc.mMutex); if (MOZ_UNLIKELY(!aSrc.mMaybeUnderlyingBuffer)) { // The other BRB is out-of-session, nothing to copy, we're done. return ProfileBufferBlockIndex{}; } const ProfileBufferIndex srcStartIndex = aSrc.mFirstReadIndex.ConvertToProfileBufferIndex(); const ProfileBufferIndex srcEndIndex = aSrc.mNextWriteIndex.ConvertToProfileBufferIndex(); const Length bytesToCopy = static_cast(srcEndIndex - srcStartIndex); if (MOZ_UNLIKELY(bytesToCopy == 0)) { // The other BRB is empty, nothing to copy, we're done. return ProfileBufferBlockIndex{}; } const Length bufferBytes = mMaybeUnderlyingBuffer->mBuffer.BufferLength().Value(); MOZ_RELEASE_ASSERT(bytesToCopy <= bufferBytes, "Entry would wrap and overwrite itself"); // We will put all copied blocks at the end of the current buffer. const ProfileBufferIndex dstStartIndex = mNextWriteIndex.ConvertToProfileBufferIndex(); // Compute where the copy will end... const ProfileBufferIndex dstEndIndex = dstStartIndex + bytesToCopy; while (dstEndIndex > mFirstReadIndex.ConvertToProfileBufferIndex() + bufferBytes) { // About to trample on an old block. ProfileBufferEntryReader reader = ReaderInBlockAt(mFirstReadIndex); mMaybeUnderlyingBuffer->mClearedBlockCount += 1; // Move the buffer reading start past this cleared block. mFirstReadIndex = ProfileBufferBlockIndex::CreateFromProfileBufferIndex( mFirstReadIndex.ConvertToProfileBufferIndex() + ULEB128Size(reader.RemainingBytes()) + reader.RemainingBytes()); } // Store the new end of buffer. mNextWriteIndex = ProfileBufferBlockIndex::CreateFromProfileBufferIndex(dstEndIndex); // Update our pushed count with the number of live blocks we are copying. mMaybeUnderlyingBuffer->mPushedBlockCount += aSrc.mMaybeUnderlyingBuffer->mPushedBlockCount - aSrc.mMaybeUnderlyingBuffer->mClearedBlockCount; auto reader = aSrc.mMaybeUnderlyingBuffer->mBuffer.EntryReaderFromTo( srcStartIndex, srcEndIndex, nullptr, nullptr); auto writer = mMaybeUnderlyingBuffer->mBuffer.EntryWriterFromTo( dstStartIndex, dstEndIndex); writer.WriteFromReader(reader, bytesToCopy); return ProfileBufferBlockIndex::CreateFromProfileBufferIndex(dstStartIndex); } // Clear all entries: Move read index to the end so that these entries cannot // be read anymore. void Clear() { baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex); ClearAllEntries(); } // Clear all entries strictly before aBlockIndex, and move read index to the // end so that these entries cannot be read anymore. void ClearBefore(ProfileBufferBlockIndex aBlockIndex) { baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex); if (!mMaybeUnderlyingBuffer) { return; } // Don't accept a not-yet-written index. One-past-the-end is ok. MOZ_ASSERT(aBlockIndex <= mNextWriteIndex); if (aBlockIndex <= mFirstReadIndex) { // Already cleared. return; } if (aBlockIndex == mNextWriteIndex) { // Right past the end, just clear everything. ClearAllEntries(); return; } // Otherwise we need to clear a subset of entries. AssertBlockIndexIsValid(aBlockIndex); // Just count skipped entries. Reader reader(*this); BlockIterator it = reader.begin(); for (; it.CurrentBlockIndex() < aBlockIndex; ++it) { MOZ_ASSERT(it.CurrentBlockIndex() < reader.end().CurrentBlockIndex()); mMaybeUnderlyingBuffer->mClearedBlockCount += 1; } MOZ_ASSERT(it.CurrentBlockIndex() == aBlockIndex); // Move read index to given index, so there's effectively no more entries // before. mFirstReadIndex = aBlockIndex; } #ifdef DEBUG void Dump() const { baseprofiler::detail::BaseProfilerMaybeAutoLock lock(mMutex); if (!mMaybeUnderlyingBuffer) { printf("empty BlocksRingBuffer\n"); return; } using ULL = unsigned long long; printf("start=%llu (%llu) end=%llu (%llu) - ", ULL(mFirstReadIndex.ConvertToProfileBufferIndex()), ULL(mFirstReadIndex.ConvertToProfileBufferIndex() & (mMaybeUnderlyingBuffer->mBuffer.BufferLength().Value() - 1)), ULL(mNextWriteIndex.ConvertToProfileBufferIndex()), ULL(mNextWriteIndex.ConvertToProfileBufferIndex() & (mMaybeUnderlyingBuffer->mBuffer.BufferLength().Value() - 1))); mMaybeUnderlyingBuffer->mBuffer.Dump(); } #endif // DEBUG private: // In DEBUG mode, assert that `aBlockIndex` is a valid index for a live block. // (Not just in range, but points exactly at the start of a block.) // Slow, so avoid it for internal checks; this is more to check what callers // provide us. void AssertBlockIndexIsValid(ProfileBufferBlockIndex aBlockIndex) const { #ifdef DEBUG mMutex.AssertCurrentThreadOwns(); MOZ_ASSERT(aBlockIndex >= mFirstReadIndex); MOZ_ASSERT(aBlockIndex < mNextWriteIndex); // Quick check (default), or slow check (change '1' to '0') below: # if 1 // Quick check that this looks like a valid block start. // Read the entry size at the start of the block. const Length entryBytes = ReaderInBlockAt(aBlockIndex).RemainingBytes(); MOZ_ASSERT(entryBytes > 0, "Empty entries are not allowed"); MOZ_ASSERT( entryBytes < mMaybeUnderlyingBuffer->mBuffer.BufferLength().Value() - ULEB128Size(entryBytes), "Entry would wrap and overwrite itself"); // The end of the block should be inside the live buffer range. MOZ_ASSERT(aBlockIndex.ConvertToProfileBufferIndex() + ULEB128Size(entryBytes) + entryBytes <= mNextWriteIndex.ConvertToProfileBufferIndex()); # else // Slow check that the index is really the start of the block. // This kills performances, as it reads from the first index until // aBlockIndex. Only use to debug issues locally. Reader reader(*this); BlockIterator it = reader.begin(); for (; it.CurrentBlockIndex() < aBlockIndex; ++it) { MOZ_ASSERT(it.CurrentBlockIndex() < reader.end().CurrentBlockIndex()); } MOZ_ASSERT(it.CurrentBlockIndex() == aBlockIndex); # endif #endif // DEBUG } // In DEBUG mode, assert that `aBlockIndex` is a valid index for a live block, // or is just past-the-end. (Not just in range, but points exactly at the // start of a block.) Slow, so avoid it for internal checks; this is more to // check what callers provide us. void AssertBlockIndexIsValidOrEnd(ProfileBufferBlockIndex aBlockIndex) const { #ifdef DEBUG mMutex.AssertCurrentThreadOwns(); if (aBlockIndex == mNextWriteIndex) { return; } AssertBlockIndexIsValid(aBlockIndex); #endif // DEBUG } // Create a reader for the block starting at aBlockIndex. ProfileBufferEntryReader ReaderInBlockAt( ProfileBufferBlockIndex aBlockIndex) const { mMutex.AssertCurrentThreadOwns(); MOZ_ASSERT(mMaybeUnderlyingBuffer.isSome()); MOZ_ASSERT(aBlockIndex >= mFirstReadIndex); MOZ_ASSERT(aBlockIndex < mNextWriteIndex); // Create a reader from the given index until the end of the buffer. ProfileBufferEntryReader reader = mMaybeUnderlyingBuffer->mBuffer.EntryReaderFromTo( aBlockIndex.ConvertToProfileBufferIndex(), mNextWriteIndex.ConvertToProfileBufferIndex(), nullptr, nullptr); // Read the block size at the beginning. const Length entryBytes = reader.ReadULEB128(); // Make sure we don't overshoot the buffer. MOZ_RELEASE_ASSERT(entryBytes <= reader.RemainingBytes()); ProfileBufferIndex nextBlockIndex = aBlockIndex.ConvertToProfileBufferIndex() + ULEB128Size(entryBytes) + entryBytes; // And reduce the reader to the entry area. Only provide a next-block-index // if it's not at the end of the buffer (i.e., there's an actual block // there). reader = mMaybeUnderlyingBuffer->mBuffer.EntryReaderFromTo( aBlockIndex.ConvertToProfileBufferIndex() + ULEB128Size(entryBytes), nextBlockIndex, aBlockIndex, (nextBlockIndex < mNextWriteIndex.ConvertToProfileBufferIndex()) ? ProfileBufferBlockIndex::CreateFromProfileBufferIndex( nextBlockIndex) : ProfileBufferBlockIndex{}); return reader; } ProfileBufferEntryReader FullBufferReader() const { mMutex.AssertCurrentThreadOwns(); if (!mMaybeUnderlyingBuffer) { return {}; } return mMaybeUnderlyingBuffer->mBuffer.EntryReaderFromTo( mFirstReadIndex.ConvertToProfileBufferIndex(), mNextWriteIndex.ConvertToProfileBufferIndex(), nullptr, nullptr); } // Clear all entries: Move read index to the end so that these entries cannot // be read anymore. void ClearAllEntries() { mMutex.AssertCurrentThreadOwns(); if (!mMaybeUnderlyingBuffer) { return; } // Mark all entries pushed so far as cleared. mMaybeUnderlyingBuffer->mClearedBlockCount = mMaybeUnderlyingBuffer->mPushedBlockCount; // Move read index to write index, so there's effectively no more entries // that can be read. (Not setting both to 0, in case user is keeping // `ProfileBufferBlockIndex`'es to old entries.) mFirstReadIndex = mNextWriteIndex; } // If there is an underlying buffer, clear all entries, and discard the // buffer. This BlocksRingBuffer will now gracefully reject all API calls, and // is in a state where a new underlying buffer may be set. void ResetUnderlyingBuffer() { mMutex.AssertCurrentThreadOwns(); if (!mMaybeUnderlyingBuffer) { return; } ClearAllEntries(); mMaybeUnderlyingBuffer.reset(); } // Used to de/serialize a BlocksRingBuffer (e.g., containing a backtrace). friend ProfileBufferEntryWriter::Serializer; friend ProfileBufferEntryReader::Deserializer; friend ProfileBufferEntryWriter::Serializer>; friend ProfileBufferEntryReader::Deserializer>; // Mutex guarding the following members. mutable baseprofiler::detail::BaseProfilerMaybeMutex mMutex; struct UnderlyingBuffer { // Create a buffer of the given length. explicit UnderlyingBuffer(PowerOfTwo aLength) : mBuffer(aLength) { MOZ_ASSERT(aLength.Value() > ULEB128MaxSize(), "Buffer should be able to contain more than a block size"); } // Take ownership of an existing buffer. UnderlyingBuffer(UniquePtr aExistingBuffer, PowerOfTwo aLength) : mBuffer(std::move(aExistingBuffer), aLength) { MOZ_ASSERT(aLength.Value() > ULEB128MaxSize(), "Buffer should be able to contain more than a block size"); } // Use an externally-owned buffer. UnderlyingBuffer(Buffer::Byte* aExternalBuffer, PowerOfTwo aLength) : mBuffer(aExternalBuffer, aLength) { MOZ_ASSERT(aLength.Value() > ULEB128MaxSize(), "Buffer should be able to contain more than a block size"); } // Only allow move-construction. UnderlyingBuffer(UnderlyingBuffer&&) = default; // Copies and move-assignment are explictly disallowed. UnderlyingBuffer(const UnderlyingBuffer&) = delete; UnderlyingBuffer& operator=(const UnderlyingBuffer&) = delete; UnderlyingBuffer& operator=(UnderlyingBuffer&&) = delete; // Underlying circular byte buffer. Buffer mBuffer; // Statistics. uint64_t mPushedBlockCount = 0; uint64_t mClearedBlockCount = 0; }; // Underlying buffer, with stats. // Only valid during in-session period. Maybe mMaybeUnderlyingBuffer; // Index to the first block to be read (or cleared). Initialized to 1 because // 0 is reserved for the "empty" ProfileBufferBlockIndex value. Kept between // sessions, so that stored indices from one session will be gracefully denied // in future sessions. ProfileBufferBlockIndex mFirstReadIndex = ProfileBufferBlockIndex::CreateFromProfileBufferIndex( ProfileBufferIndex(1)); // Index where the next new block should be allocated. Initialized to 1 // because 0 is reserved for the "empty" ProfileBufferBlockIndex value. Kept // between sessions, so that stored indices from one session will be // gracefully denied in future sessions. ProfileBufferBlockIndex mNextWriteIndex = ProfileBufferBlockIndex::CreateFromProfileBufferIndex( ProfileBufferIndex(1)); }; // ---------------------------------------------------------------------------- // BlocksRingBuffer serialization // A BlocksRingBuffer can hide another one! // This will be used to store marker backtraces; They can be read back into a // UniquePtr. // 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 { static Length Bytes(const BlocksRingBuffer& aBuffer) { baseprofiler::detail::BaseProfilerMaybeAutoLock lock(aBuffer.mMutex); if (aBuffer.mMaybeUnderlyingBuffer.isNothing()) { // Out-of-session, we only need 1 byte to store a length of 0. return ULEB128Size(0); } const auto start = aBuffer.mFirstReadIndex.ConvertToProfileBufferIndex(); const auto end = aBuffer.mNextWriteIndex.ConvertToProfileBufferIndex(); const auto len = end - start; if (len == 0) { // In-session but empty, also store a length of 0. return ULEB128Size(0); } return ULEB128Size(len) + sizeof(start) + sizeof(end) + len + sizeof(aBuffer.mMaybeUnderlyingBuffer->mPushedBlockCount) + sizeof(aBuffer.mMaybeUnderlyingBuffer->mClearedBlockCount); } static void Write(ProfileBufferEntryWriter& aEW, const BlocksRingBuffer& aBuffer) { baseprofiler::detail::BaseProfilerMaybeAutoLock lock(aBuffer.mMutex); if (aBuffer.mMaybeUnderlyingBuffer.isNothing()) { // Out-of-session, only store a length of 0. aEW.WriteULEB128(0); return; } const auto start = aBuffer.mFirstReadIndex.ConvertToProfileBufferIndex(); const auto end = aBuffer.mNextWriteIndex.ConvertToProfileBufferIndex(); MOZ_ASSERT(end - start <= std::numeric_limits::max()); const auto len = static_cast(end - start); if (len == 0) { // In-session but empty, only store a length of 0. aEW.WriteULEB128(0); return; } // In-session. // Store buffer length, start and end indices. aEW.WriteULEB128(len); aEW.WriteObject(start); aEW.WriteObject(end); // Write all the bytes. auto reader = aBuffer.FullBufferReader(); aEW.WriteFromReader(reader, reader.RemainingBytes()); // And write stats. aEW.WriteObject(aBuffer.mMaybeUnderlyingBuffer->mPushedBlockCount); aEW.WriteObject(aBuffer.mMaybeUnderlyingBuffer->mClearedBlockCount); } }; // A serialized BlocksRingBuffer can be read into an empty buffer (either // out-of-session, or in-session with enough room). template <> struct ProfileBufferEntryReader::Deserializer { static void ReadInto(ProfileBufferEntryReader& aER, BlocksRingBuffer& 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(); if (len == 0) { // 0-length means an "uninteresting" buffer, just return now. return; } // We have a non-empty buffer to read. if (aBuffer.BufferLength().isSome()) { // 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, attach a new memory buffer. aBuffer.Set(PowerOfTwo(len)); MOZ_ASSERT(aBuffer.BufferLength()->Value() >= len); } // Read start and end indices. const auto start = aER.ReadObject(); aBuffer.mFirstReadIndex = ProfileBufferBlockIndex::CreateFromProfileBufferIndex(start); const auto end = aER.ReadObject(); aBuffer.mNextWriteIndex = ProfileBufferBlockIndex::CreateFromProfileBufferIndex(end); MOZ_ASSERT(end - start == len); // Copy bytes into the buffer. auto writer = aBuffer.mMaybeUnderlyingBuffer->mBuffer.EntryWriterFromTo(start, end); writer.WriteFromReader(aER, end - start); MOZ_ASSERT(writer.RemainingBytes() == 0); // Finally copy stats. aBuffer.mMaybeUnderlyingBuffer->mPushedBlockCount = aER.ReadObjectmPushedBlockCount)>(); aBuffer.mMaybeUnderlyingBuffer->mClearedBlockCount = aER.ReadObjectmClearedBlockCount)>(); } // We cannot output a BlocksRingBuffer object (not copyable), use `ReadInto()` // or `aER.ReadObject>()` instead. static BlocksRingBuffer Read(ProfileBufferEntryReader& aER) = delete; }; // A BlocksRingBuffer 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> { static Length Bytes(const UniquePtr& aBufferUPtr) { if (!aBufferUPtr) { // Null pointer, treat it like an empty buffer, i.e., write length of 0. return ULEB128Size(0); } // Otherwise write the pointed-at BlocksRingBuffer (which could be // out-of-session or empty.) return SumBytes(*aBufferUPtr); } static void Write(ProfileBufferEntryWriter& aEW, const UniquePtr& aBufferUPtr) { if (!aBufferUPtr) { // Null pointer, treat it like an empty buffer, i.e., write length of 0. aEW.WriteULEB128(0); return; } // Otherwise write the pointed-at BlocksRingBuffer (which could be // out-of-session or empty.) aEW.WriteObject(*aBufferUPtr); } }; template <> struct ProfileBufferEntryReader::Deserializer> { static void ReadInto(ProfileBufferEntryReader& aER, UniquePtr& aBuffer) { aBuffer = Read(aER); } static UniquePtr Read(ProfileBufferEntryReader& aER) { UniquePtr 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(); if (len == 0) { // 0-length means an "uninteresting" buffer, just return nullptr. return bufferUPtr; } // We have a non-empty buffer. // allocate an empty BlocksRingBuffer without mutex. bufferUPtr = MakeUnique( BlocksRingBuffer::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 // BlocksRingBuffer_h