/* -*- 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 ProfileBufferChunk_h #define ProfileBufferChunk_h #include "mozilla/MemoryReporting.h" #include "mozilla/ProfileBufferIndex.h" #include "mozilla/Span.h" #include "mozilla/TimeStamp.h" #include "mozilla/UniquePtr.h" #if defined(MOZ_MEMORY) # include "mozmemory.h" #endif #include #include #include #ifdef DEBUG # include #endif namespace mozilla { // Represents a single chunk of memory, with a link to the next chunk (or null). // // A chunk is made of an internal header (which contains a public part) followed // by user-accessible bytes. // // +---------------+---------+----------------------------------------------+ // | public Header | private | memory containing user blocks | // +---------------+---------+----------------------------------------------+ // <---------------BufferBytes()------------------> // <------------------------------ChunkBytes()------------------------------> // // The chunk can reserve "blocks", but doesn't know the internal contents of // each block, it only knows where the first one starts, and where the last one // ends (which is where the next one will begin, if not already out of range). // It is up to the user to add structure to each block so that they can be // distinguished when later read. // // +---------------+---------+----------------------------------------------+ // | public Header | private | [1st block]...[last full block] | // +---------------+---------+----------------------------------------------+ // ChunkHeader().mOffsetFirstBlock ^ ^ // ChunkHeader().mOffsetPastLastBlock --' // // It is possible to attempt to reserve more than the remaining space, in which // case only what is available is returned. The caller is responsible for using // another chunk, reserving a block "tail" in it, and using both parts to // constitute a full block. (This initial tail may be empty in some chunks.) // // +---------------+---------+----------------------------------------------+ // | public Header | private | tail][1st block]...[last full block][head... | // +---------------+---------+----------------------------------------------+ // ChunkHeader().mOffsetFirstBlock ^ ^ // ChunkHeader().mOffsetPastLastBlock --' // // Each Chunk has an internal state (checked in DEBUG builds) that directs how // to use it during creation, initialization, use, end of life, recycling, and // destruction. See `State` below for details. // In particular: // - `ReserveInitialBlockAsTail()` must be called before the first `Reserve()` // after construction or recycling, even with a size of 0 (no actual tail), // - `MarkDone()` and `MarkRecycled()` must be called as appropriate. class ProfileBufferChunk { public: using Byte = uint8_t; using Length = uint32_t; using SpanOfBytes = Span; // Hint about the size of the metadata (public and private headers). // `Create()` below takes the minimum *buffer* size, so the minimum total // Chunk size is at least `SizeofChunkMetadata() + aMinBufferBytes`. [[nodiscard]] static constexpr Length SizeofChunkMetadata() { return static_cast(sizeof(InternalHeader)); } // Allocate space for a chunk with a given minimum size, and construct it. // The actual size may be higher, to match the actual space taken in the // memory pool. [[nodiscard]] static UniquePtr Create( Length aMinBufferBytes) { // We need at least one byte, to cover the always-present `mBuffer` byte. aMinBufferBytes = std::max(aMinBufferBytes, Length(1)); // Trivial struct with the same alignment as `ProfileBufferChunk`, and size // equal to that alignment, because typically the sizeof of an object is // a multiple of its alignment. struct alignas(alignof(InternalHeader)) ChunkStruct { Byte c[alignof(InternalHeader)]; }; static_assert(std::is_trivial_v, "ChunkStruct must be trivial to avoid any construction"); // Allocate an array of that struct, enough to contain the expected // `ProfileBufferChunk` (with its header+buffer). size_t count = (sizeof(InternalHeader) + aMinBufferBytes + (alignof(InternalHeader) - 1)) / alignof(InternalHeader); #if defined(MOZ_MEMORY) // Potentially expand the array to use more of the effective allocation. count = (malloc_good_size(count * sizeof(ChunkStruct)) + (sizeof(ChunkStruct) - 1)) / sizeof(ChunkStruct); #endif auto chunkStorage = MakeUnique(count); MOZ_ASSERT(reinterpret_cast(chunkStorage.get()) % alignof(InternalHeader) == 0); // After the allocation, compute the actual chunk size (including header). const size_t chunkBytes = count * sizeof(ChunkStruct); MOZ_ASSERT(chunkBytes >= sizeof(ProfileBufferChunk), "Not enough space to construct a ProfileBufferChunk"); MOZ_ASSERT(chunkBytes <= static_cast(std::numeric_limits::max())); // Compute the size of the user-accessible buffer inside the chunk. const Length bufferBytes = static_cast(chunkBytes - sizeof(InternalHeader)); MOZ_ASSERT(bufferBytes >= aMinBufferBytes, "Not enough space for minimum buffer size"); // Construct the header at the beginning of the allocated array, with the // known buffer size. new (chunkStorage.get()) ProfileBufferChunk(bufferBytes); // We now have a proper `ProfileBufferChunk` object, create the appropriate // UniquePtr for it. UniquePtr chunk{ reinterpret_cast(chunkStorage.release())}; MOZ_ASSERT( size_t(reinterpret_cast( &chunk.get()->BufferSpan()[bufferBytes - 1]) - reinterpret_cast(chunk.get())) == chunkBytes - 1, "Buffer span spills out of chunk allocation"); return chunk; } #ifdef DEBUG ~ProfileBufferChunk() { MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::InUse); MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Full); MOZ_ASSERT(mInternalHeader.mState == InternalHeader::State::Created || mInternalHeader.mState == InternalHeader::State::Done || mInternalHeader.mState == InternalHeader::State::Recycled); } #endif // Must be called with the first block tail (may be empty), which will be // skipped if the reader starts with this ProfileBufferChunk. [[nodiscard]] SpanOfBytes ReserveInitialBlockAsTail(Length aTailSize) { #ifdef DEBUG MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::InUse); MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Full); MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Done); MOZ_ASSERT(mInternalHeader.mState == InternalHeader::State::Created || mInternalHeader.mState == InternalHeader::State::Recycled); mInternalHeader.mState = InternalHeader::State::InUse; #endif mInternalHeader.mHeader.mOffsetFirstBlock = aTailSize; mInternalHeader.mHeader.mOffsetPastLastBlock = aTailSize; mInternalHeader.mHeader.mStartTimeStamp = TimeStamp::Now(); return SpanOfBytes(&mBuffer, aTailSize); } struct ReserveReturn { SpanOfBytes mSpan; ProfileBufferBlockIndex mBlockRangeIndex; }; // Reserve a block of up to `aBlockSize` bytes, and return a Span to it, and // its starting index. The actual size may be smaller, if the block cannot fit // in the remaining space. [[nodiscard]] ReserveReturn ReserveBlock(Length aBlockSize) { MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Created); MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Full); MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Done); MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Recycled); MOZ_ASSERT(mInternalHeader.mState == InternalHeader::State::InUse); MOZ_ASSERT(RangeStart() != 0, "Expected valid range start before first Reserve()"); const Length blockOffset = mInternalHeader.mHeader.mOffsetPastLastBlock; Length reservedSize = aBlockSize; if (MOZ_UNLIKELY(aBlockSize >= RemainingBytes())) { reservedSize = RemainingBytes(); #ifdef DEBUG mInternalHeader.mState = InternalHeader::State::Full; #endif } mInternalHeader.mHeader.mOffsetPastLastBlock += reservedSize; mInternalHeader.mHeader.mBlockCount += 1; return {SpanOfBytes(&mBuffer + blockOffset, reservedSize), ProfileBufferBlockIndex::CreateFromProfileBufferIndex( mInternalHeader.mHeader.mRangeStart + blockOffset)}; } // When a chunk will not be used to store more blocks (because it is full, or // because the profiler will not add more data), it should be marked "done". // Access to its content is still allowed. void MarkDone() { #ifdef DEBUG MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Created); MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Done); MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Recycled); MOZ_ASSERT(mInternalHeader.mState == InternalHeader::State::InUse || mInternalHeader.mState == InternalHeader::State::Full); mInternalHeader.mState = InternalHeader::State::Done; #endif mInternalHeader.mHeader.mDoneTimeStamp = TimeStamp::Now(); } // A "Done" chunk may be recycled, to avoid allocating a new one. void MarkRecycled() { #ifdef DEBUG // We also allow Created and already-Recycled chunks to be recycled, this // way it's easier to recycle chunks when their state is not easily // trackable. MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::InUse); MOZ_ASSERT(mInternalHeader.mState != InternalHeader::State::Full); MOZ_ASSERT(mInternalHeader.mState == InternalHeader::State::Created || mInternalHeader.mState == InternalHeader::State::Done || mInternalHeader.mState == InternalHeader::State::Recycled); mInternalHeader.mState = InternalHeader::State::Recycled; #endif // Reset all header fields, in case this recycled chunk gets read. mInternalHeader.mHeader.Reset(); } // Public header, meant to uniquely identify a chunk, it may be shared with // other processes to coordinate global memory handling. struct Header { explicit Header(Length aBufferBytes) : mBufferBytes(aBufferBytes) {} // Reset all members to their as-new values (apart from the buffer size, // which cannot change), ready for re-use. void Reset() { mOffsetFirstBlock = 0; mOffsetPastLastBlock = 0; mStartTimeStamp = TimeStamp{}; mDoneTimeStamp = TimeStamp{}; mBlockCount = 0; mRangeStart = 0; mProcessId = 0; } // Note: Part of the ordering of members below is to avoid unnecessary // padding. // Members managed by the ProfileBufferChunk. // Offset of the first block (past the initial tail block, which may be 0). Length mOffsetFirstBlock = 0; // Offset past the last byte of the last reserved block // It may be past mBufferBytes when last block continues in the next // ProfileBufferChunk. It may be before mBufferBytes if ProfileBufferChunk // is marked "Done" before the end is reached. Length mOffsetPastLastBlock = 0; // Timestamp when the buffer becomes in-use, ready to record data. TimeStamp mStartTimeStamp; // Timestamp when the buffer is "Done" (which happens when the last block is // written). This will be used to find and discard the oldest // ProfileBufferChunk. TimeStamp mDoneTimeStamp; // Number of bytes in the buffer, set once at construction time. const Length mBufferBytes; // Number of reserved blocks (including final one even if partial, but // excluding initial tail). Length mBlockCount = 0; // Meta-data set by the user. // Index of the first byte of this ProfileBufferChunk, relative to all // Chunks for this process. Index 0 is reserved as nullptr-like index, // mRangeStart should be set to a non-0 value before the first `Reserve()`. ProfileBufferIndex mRangeStart = 0; // Process writing to this ProfileBufferChunk. int mProcessId = 0; // A bit of spare space (necessary here because of the alignment due to // other members), may be later repurposed for extra data. const int mPADDING = 0; }; [[nodiscard]] const Header& ChunkHeader() const { return mInternalHeader.mHeader; } [[nodiscard]] Length BufferBytes() const { return ChunkHeader().mBufferBytes; } // Total size of the chunk (buffer + header). [[nodiscard]] Length ChunkBytes() const { return static_cast(sizeof(InternalHeader)) + BufferBytes(); } // Size of external resources, in this case all the following chunks. [[nodiscard]] size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { const ProfileBufferChunk* const next = GetNext(); return next ? next->SizeOfIncludingThis(aMallocSizeOf) : 0; } // Size of this chunk and all following ones. [[nodiscard]] size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { // Just in case `aMallocSizeOf` falls back on just `sizeof`, make sure we // account for at least the actual Chunk requested allocation size. return std::max(aMallocSizeOf(this), ChunkBytes()) + SizeOfExcludingThis(aMallocSizeOf); } [[nodiscard]] Length RemainingBytes() const { return BufferBytes() - OffsetPastLastBlock(); } [[nodiscard]] Length OffsetFirstBlock() const { return ChunkHeader().mOffsetFirstBlock; } [[nodiscard]] Length OffsetPastLastBlock() const { return ChunkHeader().mOffsetPastLastBlock; } [[nodiscard]] Length BlockCount() const { return ChunkHeader().mBlockCount; } [[nodiscard]] int ProcessId() const { return ChunkHeader().mProcessId; } void SetProcessId(int aProcessId) { mInternalHeader.mHeader.mProcessId = aProcessId; } // Global range index at the start of this Chunk. [[nodiscard]] ProfileBufferIndex RangeStart() const { return ChunkHeader().mRangeStart; } void SetRangeStart(ProfileBufferIndex aRangeStart) { mInternalHeader.mHeader.mRangeStart = aRangeStart; } // Get a read-only Span to the buffer. It is up to the caller to decypher the // contents, based on known offsets and the internal block structure. [[nodiscard]] Span BufferSpan() const { return Span(&mBuffer, BufferBytes()); } [[nodiscard]] Byte ByteAt(Length aOffset) const { MOZ_ASSERT(aOffset < OffsetPastLastBlock()); return *(&mBuffer + aOffset); } [[nodiscard]] ProfileBufferChunk* GetNext() { return mInternalHeader.mNext.get(); } [[nodiscard]] const ProfileBufferChunk* GetNext() const { return mInternalHeader.mNext.get(); } [[nodiscard]] UniquePtr ReleaseNext() { return std::move(mInternalHeader.mNext); } void InsertNext(UniquePtr&& aChunk) { if (!aChunk) { return; } aChunk->SetLast(ReleaseNext()); mInternalHeader.mNext = std::move(aChunk); } // Find the last chunk in this chain (it may be `this`). [[nodiscard]] ProfileBufferChunk* Last() { ProfileBufferChunk* chunk = this; for (;;) { ProfileBufferChunk* next = chunk->GetNext(); if (!next) { return chunk; } chunk = next; } } [[nodiscard]] const ProfileBufferChunk* Last() const { const ProfileBufferChunk* chunk = this; for (;;) { const ProfileBufferChunk* next = chunk->GetNext(); if (!next) { return chunk; } chunk = next; } } void SetLast(UniquePtr&& aChunk) { if (!aChunk) { return; } Last()->mInternalHeader.mNext = std::move(aChunk); } // Join two possibly-null chunk lists. [[nodiscard]] static UniquePtr Join( UniquePtr&& aFirst, UniquePtr&& aLast) { if (aFirst) { aFirst->SetLast(std::move(aLast)); return std::move(aFirst); } return std::move(aLast); } #ifdef DEBUG void Dump(std::FILE* aFile = stdout) const { fprintf(aFile, "Chunk[%p] chunkSize=%u bufferSize=%u state=%s rangeStart=%u " "firstBlockOffset=%u offsetPastLastBlock=%u blockCount=%u", this, unsigned(ChunkBytes()), unsigned(BufferBytes()), mInternalHeader.StateString(), unsigned(RangeStart()), unsigned(OffsetFirstBlock()), unsigned(OffsetPastLastBlock()), unsigned(BlockCount())); const auto len = OffsetPastLastBlock(); constexpr unsigned columns = 16; unsigned char ascii[columns + 1]; ascii[columns] = '\0'; for (Length i = 0; i < len; ++i) { if (i % columns == 0) { fprintf(aFile, "\n %4u=0x%03x:", unsigned(i), unsigned(i)); for (unsigned a = 0; a < columns; ++a) { ascii[a] = ' '; } } unsigned char sep = ' '; if (i == OffsetFirstBlock()) { if (i == OffsetPastLastBlock()) { sep = '#'; } else { sep = '['; } } else if (i == OffsetPastLastBlock()) { sep = ']'; } unsigned char c = *(&mBuffer + i); fprintf(aFile, "%c%02x", sep, c); if (i == len - 1) { if (i + 1 == OffsetPastLastBlock()) { // Special case when last block ends right at the end. fprintf(aFile, "]"); } else { fprintf(aFile, " "); } } else if (i % columns == columns - 1) { fprintf(aFile, " "); } ascii[i % columns] = (c >= ' ' && c <= '~') ? c : '.'; if (i % columns == columns - 1) { fprintf(aFile, " %s", ascii); } } if (len % columns < columns - 1) { for (Length i = len % columns; i < columns; ++i) { fprintf(aFile, " "); } fprintf(aFile, " %s", ascii); } fprintf(aFile, "\n"); } #endif // DEBUG private: // ProfileBufferChunk constructor. Use static `Create()` to allocate and // construct a ProfileBufferChunk. explicit ProfileBufferChunk(Length aBufferBytes) : mInternalHeader(aBufferBytes) {} // This internal header starts with the public `Header`, and adds some data // only necessary for local handling. // This encapsulation is also necessary to perform placement-new in // `Create()`. struct InternalHeader { explicit InternalHeader(Length aBufferBytes) : mHeader(aBufferBytes) {} Header mHeader; UniquePtr mNext; #ifdef DEBUG enum class State { Created, // Self-set. Just constructed, waiting for initial block tail. InUse, // Ready to accept blocks. Full, // Self-set. Blocks reach the end (or further). Done, // Blocks won't be added anymore. Recycled // Still full of data, but expecting an initial block tail. }; State mState = State::Created; // Transition table: (X=unexpected) // Method \ State Created InUse Full Done Recycled // ReserveInitialBlockAsTail InUse X X X InUse // Reserve X InUse/Full X X X // MarkDone X Done Done X X // MarkRecycled X X X Recycled X // destructor ok X X ok ok const char* StateString() const { switch (mState) { case State::Created: return "Created"; case State::InUse: return "InUse"; case State::Full: return "Full"; case State::Done: return "Done"; case State::Recycled: return "Recycled"; default: return "?"; } } #else // DEBUG const char* StateString() const { return "(non-DEBUG)"; } #endif }; InternalHeader mInternalHeader; // KEEP THIS LAST! // First byte of the buffer. Note that ProfileBufferChunk::Create allocates a // bigger block, such that `mBuffer` is the first of `mBufferBytes` available // bytes. // The initialization is not strictly needed, because bytes should only be // read after they have been written and `mOffsetPastLastBlock` has been // updated. However: // - Reviewbot complains that it's not initialized. // - It's cheap to initialize one byte. // - In the worst case (reading does happen), zero is not a valid entry size // and should get caught in entry readers. Byte mBuffer = '\0'; }; } // namespace mozilla #endif // ProfileBufferChunk_h