blob: 2df9c2006ab84ed014aaea28f1d91d9348220db2 [file]
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "src/tracing/v2/shared_ring_buffer_reader.h"
#include <stdint.h>
#include "perfetto/base/logging.h"
#include "src/tracing/v2/shared_ring_buffer.h"
#include "src/tracing/v2/shared_ring_buffer_abi.h"
namespace perfetto::tracing_v2 {
namespace {
// Bound attempts at one position. For M = kMaxFragmentsPerChunk:
// - Each fragment can defeat the reader's CAS twice:
// 1. Publication: BeingWritten(N) -> Complete(N + 1).
// 2. Reuse: Complete(N + 1) -> BeingWritten(N + 1).
// Allow M attempts for each, giving 2 * M.
// - Add one for the initial Free -> BeingWritten claim.
// - Add one for the reader's successful CAS, giving 2 * M + 2.
//
// This is conservative. SharedRingBufferWriter stops reusing at M fragments,
// so there are at most M - 1 reuses. Zero-byte fragments still use the count.
// FinishCurrentChunk() drops the cached handle even when abandoning a fragment.
// The limit also bounds work if another producer keeps changing the state
// without increasing the fragment count.
constexpr uint32_t kMaxAttemptsPerPosition = 2 * kMaxFragmentsPerChunk + 2;
// For the protocol-error log only.
const char* ChunkStateName(ChunkState state) {
switch (state) {
case ChunkState::kFree:
return "Free";
case ChunkState::kBeingWritten:
return "BeingWritten";
case ChunkState::kComplete:
return "Complete";
case ChunkState::kRewriteRequested:
return "RewriteRequested";
case ChunkState::kRewriteAcknowledged:
return "RewriteAcknowledged";
case ChunkState::kReserved5:
case ChunkState::kReserved6:
case ChunkState::kReserved7:
return "reserved";
}
return "unknown";
}
} // namespace
SharedRingBufferReader::Delegate::~Delegate() = default;
SharedRingBufferReader::SharedRingBufferReader(SharedRingBuffer* ring,
Delegate* delegate)
: ring_(ring),
delegate_(delegate),
num_chunks_(ring->num_chunks()),
chunk_size_(ring->chunk_size()) {
// Reserve enough scratch for any chunk format so draining does not allocate.
copied_payload_.reserve(chunk_size_);
copied_fragments_.reserve(kMaxFragmentsPerChunk);
}
SharedRingBufferReader::~SharedRingBufferReader() = default;
SharedRingBufferReader::DrainResult SharedRingBufferReader::Drain(
uint32_t max_positions) {
const uint32_t start_pos = read_pos_;
DrainResult result{};
for (uint32_t i = 0; i < max_positions; ++i) {
// A failed CAS leaves read_pos_ unchanged. Retry the same position with
// a fresh state and fragment copy before it counts towards max_positions.
// Limit attempts so repeated writer updates cannot monopolize this pass.
for (uint32_t attempt = 0; attempt < kMaxAttemptsPerPosition; ++attempt) {
result.last_result = ConsumeNextPosition();
if (result.last_result != ConsumeResult::kRetryImmediately)
break;
}
// If every attempt lost, yield with another drain requested. Contention
// alone is not a protocol error. Publish earlier progress below.
if (result.last_result != ConsumeResult::kChunkRead &&
result.last_result != ConsumeResult::kPositionSkipped) {
break;
}
}
// Unsigned subtraction also counts positions across uint32_t rollover.
// Advancing from UINT32_MAX to zero consumes one position.
result.positions_consumed = read_pos_ - start_pos;
if (result.positions_consumed != 0) {
// One publication and at most one wake cover the whole pass. Until this
// point writers can only under-estimate free capacity.
//
// TODO(sashwinbalaji): benchmark useful publication batch sizes.
ring_->PublishReadPos(read_pos_);
}
return result;
}
// The shared lifecycle is in shared_ring_buffer_abi.h.
//
// The races this function has to handle:
//
// 1. Free: advance to the next wrap, racing the delayed writer's claim.
// 2. BeingWritten: copy N fragments, then request RewriteRequested(N), racing
// the writer's publication of Complete(M).
// 3. Complete: copy the fragments, then reclaim, racing the writer's reuse.
// 4. RewriteRequested: skip. The writer still owns the chunk. Once it becomes
// RewriteAcknowledged, only the reader may reclaim it on a later traversal.
//
// In the first three cases, a failed CAS leaves read_pos unchanged. Drain()
// retries immediately. Deliver the copy only after a successful transition,
// so a retry cannot deliver the same fragments twice.
//
// RewriteAcknowledged has no competing writer transition. On failure to reclaim
// it, report a protocol error.
SharedRingBufferReader::ConsumeResult
SharedRingBufferReader::ConsumeNextPosition() {
if (PERFETTO_UNLIKELY(has_protocol_error_))
return ConsumeResult::kProtocolError;
// A relaxed load is enough here. An older write_pos only shortens this
// drain pass, which is still correct. A later pass can consume the rest.
const uint32_t write_pos = ring_->LoadWritePosRelaxed();
const uint32_t outstanding = NumOutstandingPositions(write_pos, read_pos_);
if (outstanding == 0)
return ConsumeResult::kNoData;
if (PERFETTO_UNLIKELY(outstanding > num_chunks_)) {
// A legal writer cannot reserve more than num_chunks outstanding
// positions.
has_protocol_error_ = true;
PERFETTO_ELOG(
"tracing v2: stopping ring buffer reader: write_pos %u is %u "
"positions ahead of read_pos %u, which is more than the %u chunks "
"in the ring buffer",
write_pos, outstanding, read_pos_, num_chunks_);
return ConsumeResult::kProtocolError;
}
// read_pos_ is the next logical position.
const uint32_t chunk_pos = read_pos_;
const ChunkIndex chunk_idx = ChunkIndex::FromPosition(chunk_pos, num_chunks_);
// A failed compare-and-swap replaces this with the word that won.
uint32_t state_word = ring_->LoadChunkStateWordAcquire(chunk_idx);
switch (ChunkStateOf(state_word)) {
case ChunkState::kFree: {
// Check reserved bits first. Reclaiming must not hide an invalid word.
if (PERFETTO_UNLIKELY((state_word & ~kWriterIDMask) != 0))
return StopOnProtocolError("Free word has reserved bits", state_word);
// Only this reader advances the wrap. A different wrap here is an error.
const uint32_t expected_free_word =
MakeFreeStateWordForPosition(chunk_pos, num_chunks_);
if (PERFETTO_UNLIKELY(state_word != expected_free_word)) {
return StopOnProtocolError(
"Free word carries another position's wrap count", state_word);
}
if (before_state_transition_for_testing_)
before_state_transition_for_testing_();
// Advance the wrap count to consume this unclaimed reservation.
// On failure, the delayed writer claimed first. Drain() retries this
// position with the writer's BeingWritten or Complete state.
if (!ring_->TryMoveFreeChunkToNextWrap(chunk_pos, &state_word))
return ConsumeResult::kRetryImmediately;
++read_pos_;
++stats_.positions_skipped;
return ConsumeResult::kPositionSkipped;
}
case ChunkState::kBeingWritten: {
const auto status = CopyPublishedFragments(chunk_idx, state_word);
if (before_state_transition_for_testing_)
before_state_transition_for_testing_();
// Request a rewrite before read_pos_ advances, even if validation failed.
// On failure, the writer published first. Drain() retries with a fresh
// copy before it delivers fragments or reports loss for this position.
if (!ring_->TryRequestRewrite(chunk_idx, &state_word))
return ConsumeResult::kRetryImmediately;
++read_pos_;
++stats_.rewrite_requests;
return HandleCopiedChunk(status);
}
case ChunkState::kComplete: {
const auto status = CopyPublishedFragments(chunk_idx, state_word);
if (before_state_transition_for_testing_)
before_state_transition_for_testing_();
// Reclaim the chunk before read_pos_ advances, even if validation failed.
// On failure, the writer reused the chunk and can publish more fragments.
// Drain() reloads the state and copies the current published fragments.
// If the state is BeingWritten, that attempt requests a rewrite.
// Deliver fragments or report loss only after a successful transition.
if (!ring_->TryReleaseCompleteChunkAsFree(chunk_pos, &state_word))
return ConsumeResult::kRetryImmediately;
++read_pos_;
// Report loss even if the Complete chunk has no fragments. The writer
// cannot reuse the reclaimed chunk, so no later publication can report
// its flag.
//
// For BeingWritten(0), the writer still owns the chunk and transfers
// kFlagDataLoss to the relocated fragment instead.
if (status == CopiedChunkStatus::kNoFragments &&
(copied_chunk_.payload_flags & kFlagDataLoss)) {
return HandleCopiedChunk(CopiedChunkStatus::kDataLoss);
}
return HandleCopiedChunk(status);
}
case ChunkState::kRewriteRequested:
// This chunk is still awaiting rewrite acknowledgement from its owner.
// The current reservation could not claim it. Advance read_pos without
// changing the chunk's state word or delivering fragments.
++read_pos_;
++stats_.positions_skipped;
return ConsumeResult::kPositionSkipped;
case ChunkState::kRewriteAcknowledged:
// Reclaim the chunk before advancing past this unclaimed reservation.
// No fragments are delivered. The chunk becomes available for a later
// reservation.
//
// The expected word contains only the RewriteAcknowledged state bits.
// Only this reader can change it after the writer's acknowledgement.
// On failure, the word was invalid or another actor changed it.
if (!ring_->TryReleaseRewriteAcknowledgedChunkAsFree(chunk_pos,
&state_word)) {
return StopOnProtocolError(
ChunkStateOf(state_word) == ChunkState::kRewriteAcknowledged
? "RewriteAcknowledged word has payload bits set"
: "RewriteAcknowledged word changed under the reader",
state_word);
}
++read_pos_;
++stats_.positions_skipped;
return ConsumeResult::kPositionSkipped;
case ChunkState::kReserved5:
case ChunkState::kReserved6:
case ChunkState::kReserved7:
// The reader cannot safely reclaim an unknown state.
return StopOnProtocolError("reserved chunk state", state_word);
}
PERFETTO_FATAL("tracing v2: unhandled chunk state word 0x%08x", state_word);
}
SharedRingBufferReader::CopiedChunkStatus
SharedRingBufferReader::CopyPublishedFragments(ChunkIndex chunk_idx,
uint32_t state_word) {
copied_fragments_.clear();
copied_chunk_ = ChunkContents{};
copied_chunk_.writer_id = WriterIDOf(state_word);
copied_chunk_.payload_flags = PayloadFlagsOf(state_word);
const uint32_t num_fragments = NumFragmentsOf(state_word);
if (num_fragments == 0) {
// A writer may still be storing BufferID after claiming BeingWritten(0).
// Only its first fragment publication makes that store visible. Do not
// touch any bytes beyond the state word when no fragment is published.
return CopiedChunkStatus::kNoFragments;
}
// The loss flag does not identify where data was lost within the chunk.
// - Drop every published fragment, even a complete packet after the gap.
// - Do not read an unpublished fragment. The writer may relocate it.
// The caller must win the state transition before reporting the gap or
// advancing read_pos. See Delegate::OnDataLoss() for packet recovery.
if (state_word & kFlagDataLoss)
return CopiedChunkStatus::kDataLoss;
if (ChunkFormatOf(state_word) != ChunkFormat::kTargetBuffer)
return CopiedChunkStatus::kUnsupportedFormat;
// Decode first to find the payload end and the directory start.
// Until then, payload_begin is the decoder's lower bound. It prevents size
// reads from reaching the header, but does not prove the payload fits.
const uint32_t capacity = chunk_size_ - kTargetBufferPayloadOffset;
const uint8_t* chunk = ring_->chunk_at(chunk_idx);
const uint8_t* const payload_begin = chunk + kTargetBufferPayloadOffset;
const uint8_t* sizes_cursor = chunk + chunk_size_;
uint32_t total = 0;
for (uint32_t i = 0; i < num_fragments; ++i) {
const auto fragment_size =
ReadFragmentSizeReversed(payload_begin, &sizes_cursor);
if (!fragment_size || *fragment_size > capacity - total) {
return CopiedChunkStatus::kMalformed;
}
total += *fragment_size;
copied_fragments_.push_back(protozero::ConstBytes{nullptr, *fragment_size});
}
// The sizes are valid varints. Their payloads may still overlap the
// directory:
// - Count the bytes actually read, including non-minimal size encodings.
// - On overlap, reject the chunk before copying or delivering any payload.
// - sizes_cursor is local to this read attempt. Returning discards it.
// No cursor change needs to be undone.
const uint32_t sizes_bytes =
static_cast<uint32_t>(chunk + chunk_size_ - sizes_cursor);
if (total > capacity - sizes_bytes)
return CopiedChunkStatus::kMalformed;
// Copy the published payload out of shared memory.
copied_payload_.assign(payload_begin, payload_begin + total);
// Point each fragment at its copy and collect the chunk metadata.
size_t offset = 0;
for (protozero::ConstBytes& fragment : copied_fragments_) {
fragment.data = copied_payload_.data() + offset;
offset += fragment.size;
}
copied_chunk_.target_buffer = LoadTargetBufferId(chunk);
copied_chunk_.fragments = copied_fragments_.data();
copied_chunk_.num_fragments = num_fragments;
return CopiedChunkStatus::kReady;
}
SharedRingBufferReader::ConsumeResult SharedRingBufferReader::HandleCopiedChunk(
CopiedChunkStatus status) {
switch (status) {
case CopiedChunkStatus::kReady:
++stats_.chunks_read;
stats_.bytes_read += copied_payload_.size();
delegate_->OnChunkRead(copied_chunk_);
return ConsumeResult::kChunkRead;
case CopiedChunkStatus::kMalformed:
++stats_.malformed_chunks;
delegate_->OnDataLoss(copied_chunk_.writer_id);
break;
case CopiedChunkStatus::kUnsupportedFormat:
++stats_.unsupported_format_chunks;
delegate_->OnDataLoss(copied_chunk_.writer_id);
break;
case CopiedChunkStatus::kDataLoss:
++stats_.data_loss_chunks;
delegate_->OnDataLoss(copied_chunk_.writer_id);
break;
case CopiedChunkStatus::kNoFragments:
break;
}
++stats_.positions_skipped;
return ConsumeResult::kPositionSkipped;
}
SharedRingBufferReader::ConsumeResult
SharedRingBufferReader::StopOnProtocolError(const char* reason,
uint32_t state_word) {
// Stop rather than trusting a malformed word from a producer. Log once.
has_protocol_error_ = true;
PERFETTO_ELOG(
"tracing v2: stopping ring buffer reader at position %u: %s "
"(chunk state word 0x%08x, %s)",
read_pos_, reason, state_word, ChunkStateName(ChunkStateOf(state_word)));
return ConsumeResult::kProtocolError;
}
} // namespace perfetto::tracing_v2