Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
#ifndef ProfileChunkedBufferDetail_h
#define ProfileChunkedBufferDetail_h
#include "mozilla/Assertions.h"
#include "mozilla/Likely.h"
#include "mozilla/ProfileBufferChunk.h"
#include "mozilla/ProfileBufferEntrySerialization.h"
namespace mozilla::profiler::detail {
// Internal accessor pointing at a position inside a chunk.
// It can handle two groups of chunks (typically the extant chunks stored in
// the store manager, and the current chunk).
// The main operations are:
// - ReadEntrySize() to read an entry size, 0 means failure.
// - operator+=(Length) to skip a number of bytes.
// - EntryReader() creates an entry reader at the current position for a given
// size (it may fail with an empty reader), and skips the entry.
// Note that there is no "past-the-end" position -- as soon as InChunkPointer
// reaches the end, it becomes effectively null.
class InChunkPointer {
public:
using Byte = ProfileBufferChunk::Byte;
using Length = ProfileBufferChunk::Length;
// Nullptr-like InChunkPointer, may be used as end iterator.
InChunkPointer()
: mChunk(nullptr), mNextChunkGroup(nullptr), mOffsetInChunk(0) {}
// InChunkPointer over one or two chunk groups, pointing at the given
// block index (if still in range).
// This constructor should only be used with *trusted* block index values!
InChunkPointer(const ProfileBufferChunk* aChunk,
const ProfileBufferChunk* aNextChunkGroup,
ProfileBufferBlockIndex aBlockIndex)
: mChunk(aChunk), mNextChunkGroup(aNextChunkGroup) {
if (mChunk) {
mOffsetInChunk = mChunk->OffsetFirstBlock();
Adjust();
} else if (mNextChunkGroup) {
mChunk = mNextChunkGroup;
mNextChunkGroup = nullptr;
mOffsetInChunk = mChunk->OffsetFirstBlock();
Adjust();
} else {
mOffsetInChunk = 0;
}
// Try to advance to given position.
if (!AdvanceToGlobalRangePosition(aBlockIndex)) {
// Block does not exist anymore (or block doesn't look valid), reset the
// in-chunk pointer.
mChunk = nullptr;
mNextChunkGroup = nullptr;
}
}
// InChunkPointer over one or two chunk groups, will start at the first
// block (if any). This may be slow, so avoid using it too much.
InChunkPointer(const ProfileBufferChunk* aChunk,
const ProfileBufferChunk* aNextChunkGroup,
ProfileBufferIndex aIndex = ProfileBufferIndex(0))
: mChunk(aChunk), mNextChunkGroup(aNextChunkGroup) {
if (mChunk) {
mOffsetInChunk = mChunk->OffsetFirstBlock();
Adjust();
} else if (mNextChunkGroup) {
mChunk = mNextChunkGroup;
mNextChunkGroup = nullptr;
mOffsetInChunk = mChunk->OffsetFirstBlock();
Adjust();
} else {
mOffsetInChunk = 0;
}
// Try to advance to given position.
if (!AdvanceToGlobalRangePosition(aIndex)) {
// Block does not exist anymore, reset the in-chunk pointer.
mChunk = nullptr;
mNextChunkGroup = nullptr;
}
}
// Compute the current position in the global range.
// 0 if null (including if we're reached the end).
[[nodiscard]] ProfileBufferIndex GlobalRangePosition() const {
if (IsNull()) {
return 0;
}
return mChunk->RangeStart() + mOffsetInChunk;
}
// Move InChunkPointer forward to the block at the given global block
// position, which is assumed to be valid exactly -- but it may be obsolete.
// 0 stays where it is (if valid already).
// MOZ_ASSERTs if the index is invalid.
[[nodiscard]] bool AdvanceToGlobalRangePosition(
ProfileBufferBlockIndex aBlockIndex) {
if (IsNull()) {
// Pointer is null already. (Not asserting because it's acceptable.)
return false;
}
if (!aBlockIndex) {
// Special null position, just stay where we are.
return ShouldPointAtValidBlock();
}
if (aBlockIndex.ConvertToProfileBufferIndex() < GlobalRangePosition()) {
// Past the requested position, stay where we are (assuming the current
// position was valid).
return ShouldPointAtValidBlock();
}
for (;;) {
if (aBlockIndex.ConvertToProfileBufferIndex() <
mChunk->RangeStart() + mChunk->OffsetPastLastBlock()) {
// Target position is in this chunk's written space, move to it.
mOffsetInChunk =
aBlockIndex.ConvertToProfileBufferIndex() - mChunk->RangeStart();
return ShouldPointAtValidBlock();
}
// Position is after this chunk, try next chunk.
GoToNextChunk();
if (IsNull()) {
return false;
}
// Skip whatever block tail there is, we don't allow pointing in the
// middle of a block.
mOffsetInChunk = mChunk->OffsetFirstBlock();
if (aBlockIndex.ConvertToProfileBufferIndex() < GlobalRangePosition()) {
// Past the requested position, meaning that the given position was in-
// between blocks -> Failure.
MOZ_ASSERT(false, "AdvanceToGlobalRangePosition - In-between blocks");
return false;
}
}
}
// Move InChunkPointer forward to the block at or after the given global
// range position.
// 0 stays where it is (if valid already).
[[nodiscard]] bool AdvanceToGlobalRangePosition(
ProfileBufferIndex aPosition) {
if (aPosition == 0) {
// Special position '0', just stay where we are.
// Success if this position is already valid.
return !IsNull();
}
for (;;) {
ProfileBufferIndex currentPosition = GlobalRangePosition();
if (currentPosition == 0) {
// Pointer is null.
return false;
}
if (aPosition <= currentPosition) {
// At or past the requested position, stay where we are.
return true;
}
if (aPosition < mChunk->RangeStart() + mChunk->OffsetPastLastBlock()) {
// Target position is in this chunk's written space, move to it.
for (;;) {
// Skip the current block.
mOffsetInChunk += ReadEntrySize();
if (mOffsetInChunk >= mChunk->OffsetPastLastBlock()) {
// Reached the end of the chunk, this can happen for the last
// block, let's just continue to the next chunk.
break;
}
if (aPosition <= mChunk->RangeStart() + mOffsetInChunk) {
// We're at or after the position, return at this block position.
return true;
}
}
}
// Position is after this chunk, try next chunk.
GoToNextChunk();
if (IsNull()) {
return false;
}
// Skip whatever block tail there is, we don't allow pointing in the
// middle of a block.
mOffsetInChunk = mChunk->OffsetFirstBlock();
}
}
[[nodiscard]] Byte ReadByte() {
MOZ_ASSERT(!IsNull());
MOZ_ASSERT(mOffsetInChunk < mChunk->OffsetPastLastBlock());
Byte byte = mChunk->ByteAt(mOffsetInChunk);
if (MOZ_UNLIKELY(++mOffsetInChunk == mChunk->OffsetPastLastBlock())) {
Adjust();
}
return byte;
}
// Read and skip a ULEB128-encoded size.
// 0 means failure (0-byte entries are not allowed.)
// Note that this doesn't guarantee that there are actually that many bytes
// available to read! (EntryReader() below may gracefully fail.)
[[nodiscard]] Length ReadEntrySize() {
ULEB128Reader<Length> reader;
if (IsNull()) {
return 0;
}
for (;;) {
const bool isComplete = reader.FeedByteIsComplete(ReadByte());
if (MOZ_UNLIKELY(IsNull())) {
// End of chunks, so there's no actual entry after this anyway.
return 0;
}
if (MOZ_LIKELY(isComplete)) {
if (MOZ_UNLIKELY(reader.Value() > mChunk->BufferBytes())) {
// Don't allow entries larger than a chunk.
return 0;
}
return reader.Value();
}
}
}
InChunkPointer& operator+=(Length aLength) {
MOZ_ASSERT(!IsNull());
mOffsetInChunk += aLength;
Adjust();
return *this;
}
[[nodiscard]] ProfileBufferEntryReader EntryReader(Length aLength) {
if (IsNull() || aLength == 0) {
return ProfileBufferEntryReader();
}
MOZ_ASSERT(mOffsetInChunk < mChunk->OffsetPastLastBlock());
// We should be pointing at the entry, past the entry size.
const ProfileBufferIndex entryIndex = GlobalRangePosition();
// Verify that there's enough space before for the size (starting at index
// 1 at least).
MOZ_ASSERT(entryIndex >= 1u + ULEB128Size(aLength));
const Length remaining = mChunk->OffsetPastLastBlock() - mOffsetInChunk;
Span<const Byte> mem0 = mChunk->BufferSpan();
mem0 = mem0.From(mOffsetInChunk);
if (aLength <= remaining) {
// Move to the end of this block, which could make this null if we have
// reached the end of all buffers.
*this += aLength;
return ProfileBufferEntryReader(
mem0.To(aLength),
// Block starts before the entry size.
ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
entryIndex - ULEB128Size(aLength)),
// Block ends right after the entry (could be null for last entry).
ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
GlobalRangePosition()));
}
// We need to go to the next chunk for the 2nd part of this block.
GoToNextChunk();
if (IsNull()) {
return ProfileBufferEntryReader();
}
Span<const Byte> mem1 = mChunk->BufferSpan();
const Length tail = aLength - remaining;
MOZ_ASSERT(tail <= mChunk->BufferBytes());
MOZ_ASSERT(tail == mChunk->OffsetFirstBlock());
// We are in the correct chunk, move the offset to the end of the block.
mOffsetInChunk = tail;
// And adjust as needed, which could make this null if we have reached the
// end of all buffers.
Adjust();
return ProfileBufferEntryReader(
mem0, mem1.To(tail),
// Block starts before the entry size.
ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
entryIndex - ULEB128Size(aLength)),
// Block ends right after the entry (could be null for last entry).
ProfileBufferBlockIndex::CreateFromProfileBufferIndex(
GlobalRangePosition()));
}
[[nodiscard]] bool IsNull() const { return !mChunk; }
[[nodiscard]] bool operator==(const InChunkPointer& aOther) const {
if (IsNull() || aOther.IsNull()) {
return IsNull() && aOther.IsNull();
}
return mChunk == aOther.mChunk && mOffsetInChunk == aOther.mOffsetInChunk;
}
[[nodiscard]] bool operator!=(const InChunkPointer& aOther) const {
return !(*this == aOther);
}
[[nodiscard]] Byte operator*() const {
MOZ_ASSERT(!IsNull());
MOZ_ASSERT(mOffsetInChunk < mChunk->OffsetPastLastBlock());
return mChunk->ByteAt(mOffsetInChunk);
}
InChunkPointer& operator++() {
MOZ_ASSERT(!IsNull());
MOZ_ASSERT(mOffsetInChunk < mChunk->OffsetPastLastBlock());
if (MOZ_UNLIKELY(++mOffsetInChunk == mChunk->OffsetPastLastBlock())) {
mOffsetInChunk = 0;
GoToNextChunk();
Adjust();
}
return *this;
}
private:
void GoToNextChunk() {
MOZ_ASSERT(!IsNull());
const ProfileBufferIndex expectedNextRangeStart =
mChunk->RangeStart() + mChunk->BufferBytes();
mChunk = mChunk->GetNext();
if (!mChunk) {
// Reached the end of the current chunk group, try the next one (which
// may be null too, especially on the 2nd try).
mChunk = mNextChunkGroup;
mNextChunkGroup = nullptr;
}
if (mChunk && mChunk->RangeStart() == 0) {
// Reached a chunk without a valid (non-null) range start, assume there
// are only unused chunks from here on.
mChunk = nullptr;
}
MOZ_ASSERT(!mChunk || mChunk->RangeStart() == expectedNextRangeStart,
"We don't handle discontinuous buffers (yet)");
// Non-DEBUG fallback: Stop reading past discontinuities.
// (They should be rare, only happening on temporary OOMs.)
// TODO: Handle discontinuities (by skipping over incomplete blocks).
if (mChunk && mChunk->RangeStart() != expectedNextRangeStart) {
mChunk = nullptr;
}
}
// We want `InChunkPointer` to always point at a valid byte (or be null).
// After some operations, `mOffsetInChunk` may point past the end of the
// current `mChunk`, in which case we need to adjust our position to be inside
// the appropriate chunk. E.g., if we're 10 bytes after the end of the current
// chunk, we should end up at offset 10 in the next chunk.
// Note that we may "fall off" the last chunk and make this `InChunkPointer`
// effectively null.
void Adjust() {
while (mChunk && mOffsetInChunk >= mChunk->OffsetPastLastBlock()) {
// TODO: Try to adjust offset between chunks relative to mRangeStart
// differences. But we don't handle discontinuities yet.
if (mOffsetInChunk < mChunk->BufferBytes()) {
mOffsetInChunk -= mChunk->BufferBytes();
} else {
mOffsetInChunk -= mChunk->OffsetPastLastBlock();
}
GoToNextChunk();
}
}
// Check if the current position is likely to point at a valid block.
// (Size should be reasonable, and block should fully fit inside buffer.)
// MOZ_ASSERTs on failure, to catch incorrect uses of block indices (which
// should only point at valid blocks if still in range). Non-asserting build
// fallback should still be handled.
[[nodiscard]] bool ShouldPointAtValidBlock() const {
if (IsNull()) {
// Pointer is null, no blocks here.
MOZ_ASSERT(false, "ShouldPointAtValidBlock - null pointer");
return false;
}
// Use a copy, so we don't modify `*this`.
InChunkPointer pointer = *this;
// Try to read the entry size.
Length entrySize = pointer.ReadEntrySize();
if (entrySize == 0) {
// Entry size of zero means we read 0 or a way-too-big value.
MOZ_ASSERT(false, "ShouldPointAtValidBlock - invalid size");
return false;
}
// See if the last byte of the entry is still inside the buffer.
pointer += entrySize - 1;
MOZ_ASSERT(!pointer.IsNull(),
"ShouldPointAtValidBlock - past end of buffer");
return !pointer.IsNull();
}
const ProfileBufferChunk* mChunk;
const ProfileBufferChunk* mNextChunkGroup;
Length mOffsetInChunk;
};
} // namespace mozilla::profiler::detail
#endif // ProfileChunkedBufferDetail_h