blob: a8937bfbfb392e3ea152c7e4dfe5da693be916f1 [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 <string.h>
#include <memory>
#include <string>
#include <vector>
#include "src/tracing/v2/shared_ring_buffer.h"
#include "src/tracing/v2/shared_ring_buffer_abi.h"
#include "src/tracing/v2/shared_ring_buffer_test_utils.h"
#include "src/tracing/v2/shared_ring_buffer_writer.h"
#include "test/gtest_and_gmock.h"
namespace perfetto::tracing_v2 {
namespace {
using Internals = test::SharedRingBufferInternalsForTest;
using BeginFragmentResult = SharedRingBufferWriter::BeginFragmentResult;
using EndFragmentResult = SharedRingBufferWriter::EndFragmentResult;
using ConsumeResult = SharedRingBufferReader::ConsumeResult;
using test::MakeWriter;
using test::WriteFragment;
constexpr WriterID kWriterA = 7;
constexpr WriterID kWriterB = 8;
constexpr BufferID kBuffer = 0x1234;
struct ReadChunk {
WriterID writer_id = 0;
BufferID target_buffer = 0;
uint32_t payload_flags = 0;
std::vector<std::string> fragments;
};
class RecordingDelegate : public SharedRingBufferReader::Delegate {
public:
void OnChunkRead(
const SharedRingBufferReader::ChunkContents& contents) override {
ReadChunk chunk;
chunk.writer_id = contents.writer_id;
chunk.target_buffer = contents.target_buffer;
chunk.payload_flags = contents.payload_flags;
for (uint32_t i = 0; i < contents.num_fragments; ++i) {
chunk.fragments.emplace_back(
reinterpret_cast<const char*>(contents.fragments[i].data),
contents.fragments[i].size);
}
chunks.push_back(std::move(chunk));
}
void OnDataLoss(WriterID writer_id) override {
writers_with_data_loss.push_back(writer_id);
}
std::vector<std::string> AllFragments() const {
std::vector<std::string> all;
for (const ReadChunk& chunk : chunks)
all.insert(all.end(), chunk.fragments.begin(), chunk.fragments.end());
return all;
}
std::vector<ReadChunk> chunks;
std::vector<WriterID> writers_with_data_loss;
};
// ---------------------------------------------------------------------------
// The nominal path.
// ---------------------------------------------------------------------------
TEST(SharedRingBufferReaderTest, EmptyRing) {
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
EXPECT_EQ(Internals::ConsumeNextPosition(&reader), ConsumeResult::kNoData);
EXPECT_EQ(reader.read_pos(), 0u);
const SharedRingBufferReader::DrainResult result = reader.Drain(16);
EXPECT_EQ(result.positions_consumed, 0u);
EXPECT_FALSE(result.needs_another_drain());
}
TEST(SharedRingBufferReaderTest, ConsumesCompleteChunk) {
// A Complete chunk is consumed and freed for the next traversal.
test::SharedRingBufferForTesting ring(4, 512);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
SharedRingBufferWriter writer = MakeWriter(ring.get(), kWriterA, kBuffer);
ASSERT_TRUE(WriteFragment(&writer, "alpha"));
ASSERT_TRUE(WriteFragment(&writer, "beta"));
writer.FinishCurrentChunk();
EXPECT_EQ(Internals::ConsumeNextPosition(&reader), ConsumeResult::kChunkRead);
EXPECT_EQ(reader.read_pos(), 1u);
ASSERT_EQ(delegate.chunks.size(), 1u);
EXPECT_EQ(delegate.chunks[0].writer_id, kWriterA);
EXPECT_EQ(delegate.chunks[0].target_buffer, kBuffer);
EXPECT_EQ(delegate.chunks[0].fragments,
(std::vector<std::string>{"alpha", "beta"}));
EXPECT_EQ(reader.GetStats().chunks_read, 1u);
EXPECT_EQ(reader.GetStats().bytes_read, strlen("alpha") + strlen("beta"));
// The chunk is now Free with the wrap count of its next position.
EXPECT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)),
MakeFreeStateWord(1));
EXPECT_EQ(Internals::ConsumeNextPosition(&reader), ConsumeResult::kNoData);
}
TEST(SharedRingBufferReaderTest, DrainRetriesWriterClaim) {
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
ASSERT_EQ(ring->TryReserveWritePos().result,
SharedRingBuffer::ReserveResult::kReserved);
uint32_t attempts = 0;
Internals::SetBeforeStateTransitionCallback(&reader, [&] {
EXPECT_EQ(reader.read_pos(), 0u);
if (++attempts == 1) {
// Claim after the reader observed Free. Its wrap-advance CAS must fail.
ASSERT_TRUE(ring->TryAcquireChunkForWriting(
0, MakeDataStateWord(ChunkState::kBeingWritten,
ChunkFormat::kTargetBuffer, 0, 0, kWriterA)));
}
});
const auto result = reader.Drain(1);
EXPECT_EQ(attempts, 2u);
EXPECT_EQ(result.positions_consumed, 1u);
EXPECT_EQ(result.last_result, ConsumeResult::kPositionSkipped);
EXPECT_EQ(Internals::GetReadPos(ring.get()), 1u);
EXPECT_EQ(reader.GetStats().rewrite_requests, 1u);
EXPECT_TRUE(delegate.chunks.empty());
EXPECT_TRUE(delegate.writers_with_data_loss.empty());
}
TEST(SharedRingBufferReaderTest, DrainRetriesWriterPublication) {
for (bool abandon_fragment : {false, true}) {
for (bool report_loss : {false, true}) {
SCOPED_TRACE(abandon_fragment);
SCOPED_TRACE(report_loss);
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
SharedRingBufferWriter writer = MakeWriter(ring.get(), kWriterA, kBuffer);
ASSERT_TRUE(WriteFragment(&writer, "before"));
if (report_loss)
writer.RecordDataLoss();
const auto range = writer.BeginFragment(5, false);
ASSERT_EQ(range.result, BeginFragmentResult::kSuccess);
memcpy(range.begin, "after", 5);
uint32_t attempts = 0;
Internals::SetBeforeStateTransitionCallback(&reader, [&] {
EXPECT_EQ(reader.read_pos(), 0u);
EXPECT_EQ(Internals::GetReadPos(ring.get()), 0u);
EXPECT_TRUE(delegate.chunks.empty());
EXPECT_TRUE(delegate.writers_with_data_loss.empty());
if (++attempts != 1)
return;
// Publish after the reader copied "before" from BeingWritten.
const auto result = abandon_fragment ? writer.FinishCurrentChunk()
: writer.EndFragment(5, false);
ASSERT_EQ(result, EndFragmentResult::kSuccess);
});
const auto result = reader.Drain(1);
EXPECT_EQ(attempts, 2u);
EXPECT_EQ(result.positions_consumed, 1u);
EXPECT_EQ(Internals::GetReadPos(ring.get()), 1u);
if (report_loss) {
EXPECT_EQ(result.last_result, ConsumeResult::kPositionSkipped);
EXPECT_TRUE(delegate.chunks.empty());
EXPECT_EQ(delegate.writers_with_data_loss,
std::vector<WriterID>{kWriterA});
} else {
EXPECT_EQ(result.last_result, ConsumeResult::kChunkRead);
ASSERT_EQ(delegate.chunks.size(), 1u);
const std::vector<std::string> expected =
abandon_fragment ? std::vector<std::string>{"before"}
: std::vector<std::string>{"before", "after"};
EXPECT_EQ(delegate.AllFragments(), expected);
EXPECT_TRUE(delegate.writers_with_data_loss.empty());
}
}
}
}
TEST(SharedRingBufferReaderTest, DrainRetriesWriterReuse) {
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
SharedRingBufferWriter writer = MakeWriter(ring.get(), kWriterA, kBuffer);
ASSERT_TRUE(WriteFragment(&writer, "before"));
uint32_t attempts = 0;
Internals::SetBeforeStateTransitionCallback(&reader, [&] {
EXPECT_EQ(reader.read_pos(), 0u);
EXPECT_TRUE(delegate.chunks.empty());
if (++attempts == 1) {
// Reuse after the reader copied Complete. Its reclaim CAS must fail.
const auto range = writer.BeginFragment(5, false);
ASSERT_EQ(range.result, BeginFragmentResult::kSuccess);
memcpy(range.begin, "after", 5);
}
});
const auto result = reader.Drain(1);
EXPECT_EQ(attempts, 2u);
EXPECT_EQ(result.positions_consumed, 1u);
EXPECT_EQ(result.last_result, ConsumeResult::kChunkRead);
ASSERT_EQ(delegate.chunks.size(), 1u);
EXPECT_EQ(delegate.AllFragments(), std::vector<std::string>{"before"});
EXPECT_EQ(reader.GetStats().rewrite_requests, 1u);
Internals::SetBeforeStateTransitionCallback(&reader, {});
ASSERT_EQ(writer.EndFragment(5, false), EndFragmentResult::kSuccess);
EXPECT_EQ(writer.GetStats().relocations, 1u);
reader.Drain(4);
EXPECT_EQ(delegate.AllFragments(),
(std::vector<std::string>{"before", "after"}));
}
TEST(SharedRingBufferReaderTest, DrainRetriesUntilFragmentLimit) {
test::SharedRingBufferForTesting ring(4, 512);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
SharedRingBufferWriter writer = MakeWriter(ring.get(), kWriterA, kBuffer);
ASSERT_EQ(writer.BeginFragment(0, false).result,
BeginFragmentResult::kSuccess);
uint32_t attempts = 0;
uint32_t fragments_published = 0;
bool fragment_open = true;
Internals::SetBeforeStateTransitionCallback(&reader, [&] {
++attempts;
EXPECT_EQ(reader.read_pos(), 0u);
EXPECT_TRUE(delegate.chunks.empty());
if (fragments_published == kMaxFragmentsPerChunk)
return;
// Alternate publication and reuse so every CAS loses until the chunk
// reaches the fragment limit. Zero-byte fragments still use the count.
if (fragment_open) {
ASSERT_EQ(writer.EndFragment(0, false), EndFragmentResult::kSuccess);
++fragments_published;
} else {
ASSERT_EQ(writer.BeginFragment(0, false).result,
BeginFragmentResult::kSuccess);
}
fragment_open = !fragment_open;
});
const auto result = reader.Drain(1);
EXPECT_EQ(attempts, 2 * kMaxFragmentsPerChunk);
EXPECT_EQ(result.positions_consumed, 1u);
EXPECT_EQ(result.last_result, ConsumeResult::kChunkRead);
ASSERT_EQ(delegate.chunks.size(), 1u);
EXPECT_EQ(delegate.AllFragments(),
std::vector<std::string>(kMaxFragmentsPerChunk));
EXPECT_FALSE(reader.has_protocol_error());
}
TEST(SharedRingBufferReaderTest, RetryLimitPublishesEarlierProgress) {
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
SharedRingBufferWriter writer = MakeWriter(ring.get(), kWriterA, kBuffer);
ASSERT_TRUE(WriteFragment(&writer, "first"));
writer.FinishCurrentChunk();
writer.RecordDataLoss();
ASSERT_TRUE(WriteFragment(&writer, "discarded"));
writer.FinishCurrentChunk();
uint32_t failed_attempts = 0;
Internals::SetBeforeStateTransitionCallback(&reader, [&] {
if (reader.read_pos() == 0)
return;
EXPECT_EQ(reader.read_pos(), 1u);
EXPECT_EQ(Internals::GetReadPos(ring.get()), 0u);
EXPECT_TRUE(delegate.writers_with_data_loss.empty());
// Exercise the scheduling limit through the low-level API. Keep changing
// state at the same fragment count, beyond what the writer class does.
const ChunkIndex chunk_idx = ChunkIndex::FromIndex(1);
uint32_t word = ring->LoadChunkStateWordAcquire(chunk_idx);
if (ChunkStateOf(word) == ChunkState::kComplete) {
ASSERT_TRUE(ring->TryReacquireChunkForWriting(chunk_idx, word));
} else {
ASSERT_EQ(ChunkStateOf(word), ChunkState::kBeingWritten);
ASSERT_TRUE(ring->TryReleaseChunkAsComplete(
chunk_idx, ReplaceChunkState(word, ChunkState::kComplete), &word));
}
++failed_attempts;
});
const auto result = reader.Drain(2);
EXPECT_EQ(failed_attempts, 2 * kMaxFragmentsPerChunk + 2);
EXPECT_EQ(result.last_result, ConsumeResult::kRetryImmediately);
EXPECT_TRUE(result.needs_another_drain());
EXPECT_EQ(result.positions_consumed, 1u);
EXPECT_EQ(reader.read_pos(), 1u);
EXPECT_EQ(Internals::GetReadPos(ring.get()), 1u);
EXPECT_EQ(delegate.AllFragments(), std::vector<std::string>{"first"});
EXPECT_TRUE(delegate.writers_with_data_loss.empty());
EXPECT_FALSE(reader.has_protocol_error());
Internals::SetBeforeStateTransitionCallback(&reader, {});
const auto next = reader.Drain(1);
EXPECT_EQ(next.positions_consumed, 1u);
EXPECT_EQ(next.last_result, ConsumeResult::kPositionSkipped);
EXPECT_EQ(Internals::GetReadPos(ring.get()), 2u);
EXPECT_EQ(delegate.writers_with_data_loss, std::vector<WriterID>{kWriterA});
EXPECT_EQ(delegate.AllFragments(), std::vector<std::string>{"first"});
}
TEST(SharedRingBufferReaderTest, DrainPublishesOnce) {
test::SharedRingBufferForTesting ring(8, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
// Four writers, one chunk each, so the pass has four positions to consume.
std::vector<std::unique_ptr<SharedRingBufferWriter>> writers;
for (uint32_t i = 0; i < 4; ++i) {
writers.push_back(std::make_unique<SharedRingBufferWriter>(
ring.get(), static_cast<WriterID>(10 + i), kBuffer));
ASSERT_TRUE(WriteFragment(writers.back().get(), "x"));
}
// The shared read_pos has not moved yet: ConsumeNextPosition() does not
// publish it.
EXPECT_EQ(Internals::GetReadPos(ring.get()), 0u);
const SharedRingBufferReader::DrainResult result = reader.Drain(16);
EXPECT_EQ(result.positions_consumed, 4u);
EXPECT_EQ(result.last_result, ConsumeResult::kNoData);
EXPECT_FALSE(result.needs_another_drain());
EXPECT_EQ(Internals::GetReadPos(ring.get()), 4u);
EXPECT_EQ(delegate.chunks.size(), 4u);
}
TEST(SharedRingBufferReaderTest, DrainBudget) {
// Drain stops at its budget and reports that work may remain.
test::SharedRingBufferForTesting ring(8, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
std::vector<std::unique_ptr<SharedRingBufferWriter>> writers;
for (uint32_t i = 0; i < 4; ++i) {
writers.push_back(std::make_unique<SharedRingBufferWriter>(
ring.get(), static_cast<WriterID>(10 + i), kBuffer));
ASSERT_TRUE(WriteFragment(writers.back().get(), "x"));
}
const SharedRingBufferReader::DrainResult first = reader.Drain(2);
EXPECT_EQ(first.positions_consumed, 2u);
EXPECT_TRUE(first.needs_another_drain());
EXPECT_EQ(Internals::GetReadPos(ring.get()), 2u);
const SharedRingBufferReader::DrainResult second = reader.Drain(16);
EXPECT_EQ(second.positions_consumed, 2u);
EXPECT_FALSE(second.needs_another_drain());
EXPECT_EQ(delegate.chunks.size(), 4u);
}
TEST(SharedRingBufferReaderTest, DrainBudgetIncludesUnclaimedReservations) {
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
SharedRingBufferWriter writer = MakeWriter(ring.get(), kWriterA, kBuffer);
// The first reservation delivers nothing. The second holds a fragment.
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 0u);
ASSERT_TRUE(WriteFragment(&writer, "published"));
ASSERT_EQ(writer.FinishCurrentChunk(), EndFragmentResult::kSuccess);
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 2u);
const auto first = reader.Drain(2);
EXPECT_EQ(first.positions_consumed, 2u);
EXPECT_TRUE(first.needs_another_drain());
EXPECT_EQ(Internals::GetReadPos(ring.get()), 2u);
EXPECT_EQ(delegate.AllFragments(), (std::vector<std::string>{"published"}));
EXPECT_EQ(reader.GetStats().positions_skipped, 1u);
// The third reservation remains for the next pass, even though it is empty.
const auto second = reader.Drain(2);
EXPECT_EQ(second.positions_consumed, 1u);
EXPECT_FALSE(second.needs_another_drain());
EXPECT_EQ(Internals::GetReadPos(ring.get()), 3u);
EXPECT_EQ(delegate.chunks.size(), 1u);
EXPECT_EQ(reader.GetStats().positions_skipped, 2u);
}
TEST(SharedRingBufferReaderTest, DrainAcrossPositionRollover) {
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
constexpr uint32_t kStartPos = UINT32_MAX - 1;
Internals::SetPositions(ring.get(), kStartPos);
Internals::SetReaderPos(&reader, kStartPos);
// Each writer reserves a fresh position: UINT32_MAX - 1, UINT32_MAX, zero.
const std::vector<std::string> fragments = {"before", "at max", "after"};
for (const auto& fragment : fragments) {
SharedRingBufferWriter writer = MakeWriter(ring.get(), kWriterA, kBuffer);
ASSERT_TRUE(WriteFragment(&writer, fragment));
ASSERT_EQ(writer.FinishCurrentChunk(), EndFragmentResult::kSuccess);
}
const auto result = reader.Drain(4);
EXPECT_EQ(result.positions_consumed, 3u);
EXPECT_EQ(result.last_result, ConsumeResult::kNoData);
EXPECT_FALSE(result.needs_another_drain());
EXPECT_EQ(reader.read_pos(), 1u);
EXPECT_EQ(Internals::GetReadPos(ring.get()), 1u);
EXPECT_EQ(delegate.AllFragments(), fragments);
EXPECT_EQ(reader.Drain(4).positions_consumed, 0u);
EXPECT_EQ(delegate.chunks.size(), 3u);
}
// ---------------------------------------------------------------------------
// Unclaimed reservations.
// ---------------------------------------------------------------------------
TEST(SharedRingBufferReaderTest, UnclaimedPosition) {
// The reader must advance the chunk's wrap count before advancing read_pos
// past this unclaimed reservation. No fragments are delivered.
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
// A writer reserved position 0 and never claimed it.
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 0u);
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kPositionSkipped);
EXPECT_EQ(reader.read_pos(), 1u);
EXPECT_EQ(reader.GetStats().positions_skipped, 1u);
EXPECT_TRUE(delegate.chunks.empty());
EXPECT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)),
MakeFreeStateWord(1));
}
TEST(SharedRingBufferReaderTest, RewriteRequestedSkipped) {
// RewriteRequested is skipped without touching the word.
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
// Chunk 0 is left mid-rewrite by a writer that has not come back, and a later
// position maps onto it.
const uint32_t being_written = MakeDataStateWord(
ChunkState::kBeingWritten, ChunkFormat::kTargetBuffer, 0, 0, kWriterB);
ASSERT_TRUE(ring->TryAcquireChunkForWriting(0, being_written));
uint32_t observed = being_written;
ASSERT_TRUE(ring->TryRequestRewrite(ChunkIndex::FromIndex(0), &observed));
const uint32_t marked =
ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0));
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 0u);
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kPositionSkipped);
EXPECT_EQ(reader.read_pos(), 1u);
// Only the owning writer may leave that state, so the reader left it alone.
EXPECT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)), marked);
EXPECT_EQ(reader.GetStats().positions_skipped, 1u);
}
TEST(SharedRingBufferReaderTest, RewriteAcknowledgedReclaimed) {
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
const uint32_t being_written = MakeDataStateWord(
ChunkState::kBeingWritten, ChunkFormat::kTargetBuffer, 0, 0, kWriterB);
ASSERT_TRUE(ring->TryAcquireChunkForWriting(0, being_written));
uint32_t observed = being_written;
ASSERT_TRUE(ring->TryRequestRewrite(ChunkIndex::FromIndex(0), &observed));
ASSERT_TRUE(ring->TryAcknowledgeRewrite(
ChunkIndex::FromIndex(0),
ReplaceChunkState(being_written, ChunkState::kRewriteRequested)));
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 0u);
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kPositionSkipped);
EXPECT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)),
MakeFreeStateWord(1));
}
// ---------------------------------------------------------------------------
// Scraping a live writer.
// ---------------------------------------------------------------------------
TEST(SharedRingBufferReaderTest, ReadsPublishedFragmentsWhileWriterAppends) {
// The reader takes the published fragments of a live writer.
test::SharedRingBufferForTesting ring(4, 512);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
SharedRingBufferWriter writer = MakeWriter(ring.get(), kWriterA, kBuffer);
ASSERT_TRUE(WriteFragment(&writer, "committed"));
// The writer is inside the chunk with a fragment still open.
const auto range = writer.BeginFragment(6, false);
ASSERT_EQ(range.result, BeginFragmentResult::kSuccess);
memcpy(range.begin, "suffix", 6);
EXPECT_EQ(Internals::ConsumeNextPosition(&reader), ConsumeResult::kChunkRead);
EXPECT_EQ(reader.GetStats().rewrite_requests, 1u);
ASSERT_EQ(delegate.chunks.size(), 1u);
// Only what the writer had published, and nothing of the open fragment.
EXPECT_EQ(delegate.chunks[0].fragments,
(std::vector<std::string>{"committed"}));
EXPECT_EQ(
ChunkStateOf(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0))),
ChunkState::kRewriteRequested);
// The writer relocates its unpublished fragment, and the reader picks it up
// next pass.
ASSERT_EQ(writer.EndFragment(6, false), EndFragmentResult::kSuccess);
reader.Drain(8);
EXPECT_EQ(delegate.AllFragments(),
(std::vector<std::string>{"committed", "suffix"}));
}
TEST(SharedRingBufferReaderTest, ReadsChunkWithNoPublishedFragments) {
// With no published fragments, the reader still requests a rewrite.
test::SharedRingBufferForTesting ring(4, 512);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
SharedRingBufferWriter writer = MakeWriter(ring.get(), kWriterA, kBuffer);
ASSERT_EQ(writer.BeginFragment(4, false).result,
BeginFragmentResult::kSuccess);
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kPositionSkipped);
EXPECT_TRUE(delegate.chunks.empty());
// Nothing came out, but the old owner is still stopped from publishing
// behind the reader.
EXPECT_EQ(
ChunkStateOf(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0))),
ChunkState::kRewriteRequested);
}
// ---------------------------------------------------------------------------
// Untrusted input.
// ---------------------------------------------------------------------------
TEST(SharedRingBufferReaderTest, UnknownFormat) {
// An unknown format drops the payload but releases the chunk.
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 0u);
Internals::SetChunkStateWord(
ring.get(), ChunkIndex::FromIndex(0),
MakeDataStateWord(ChunkState::kComplete, ChunkFormat::kReservedRouting, 0,
3, kWriterB));
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kPositionSkipped);
EXPECT_EQ(reader.GetStats().unsupported_format_chunks, 1u);
EXPECT_TRUE(delegate.chunks.empty());
EXPECT_EQ(delegate.writers_with_data_loss, (std::vector<WriterID>{kWriterB}));
// The loss was reported and the chunk remains usable.
EXPECT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)),
MakeFreeStateWord(1));
EXPECT_EQ(reader.read_pos(), 1u);
}
TEST(SharedRingBufferReaderTest, SizesLargerThanChunk) {
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 0u);
// varint(0) takes one byte. 255 such entries do not fit in the 250-byte
// payload area.
Internals::SetChunkStateWord(
ring.get(), ChunkIndex::FromIndex(0),
MakeDataStateWord(ChunkState::kComplete, ChunkFormat::kTargetBuffer, 0,
255, kWriterB));
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kPositionSkipped);
EXPECT_EQ(reader.GetStats().malformed_chunks, 1u);
EXPECT_TRUE(delegate.chunks.empty());
EXPECT_EQ(delegate.writers_with_data_loss, (std::vector<WriterID>{kWriterB}));
EXPECT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)),
MakeFreeStateWord(1));
}
TEST(SharedRingBufferReaderTest, PayloadSizesOverlap) {
// Payload and size varints must not overlap.
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 0u);
// The chunk has 250 bytes after its fixed header. A 249-byte fragment fits
// by itself, but its two-byte size varint does not fit beside it.
WriteFragmentSizeReversed(ring->chunk_at(ChunkIndex::FromIndex(0)) + 256,
249);
Internals::SetChunkStateWord(
ring.get(), ChunkIndex::FromIndex(0),
MakeDataStateWord(ChunkState::kComplete, ChunkFormat::kTargetBuffer, 0, 1,
kWriterB));
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kPositionSkipped);
EXPECT_EQ(reader.GetStats().malformed_chunks, 1u);
EXPECT_EQ(delegate.writers_with_data_loss, (std::vector<WriterID>{kWriterB}));
EXPECT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)),
MakeFreeStateWord(1));
}
TEST(SharedRingBufferReaderTest, NonMinimalSizeUsesActualDirectoryBytes) {
// A three-byte size entry leaves room for 247 payload bytes.
// 248 would fit with a minimal two-byte entry, but overlaps this directory.
for (uint32_t size : {247u, 248u}) {
SCOPED_TRACE(size);
test::SharedRingBufferForTesting ring(2, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 0u);
uint8_t* chunk = ring->chunk_at(ChunkIndex::FromIndex(0));
StoreTargetBufferId(chunk, kBuffer);
memset(chunk + kTargetBufferPayloadOffset, 'x', size);
chunk[255] = static_cast<uint8_t>(size);
chunk[254] = 0x81;
chunk[253] = 0x00;
Internals::SetChunkStateWord(
ring.get(), ChunkIndex::FromIndex(0),
MakeDataStateWord(ChunkState::kComplete, ChunkFormat::kTargetBuffer, 0,
1, kWriterA));
EXPECT_EQ(reader.Drain(1).positions_consumed, 1u);
if (size == 247) {
EXPECT_EQ(delegate.AllFragments(),
std::vector<std::string>{std::string(size, 'x')});
EXPECT_TRUE(delegate.writers_with_data_loss.empty());
EXPECT_EQ(reader.GetStats().malformed_chunks, 0u);
} else {
EXPECT_TRUE(delegate.chunks.empty());
EXPECT_EQ(delegate.writers_with_data_loss,
std::vector<WriterID>{kWriterA});
EXPECT_EQ(reader.GetStats().malformed_chunks, 1u);
}
EXPECT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)),
MakeFreeStateWord(1));
}
}
TEST(SharedRingBufferReaderTest, CumulativeSizeOverflow) {
test::SharedRingBufferForTesting ring(4, 512);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 0u);
// Two fragments claiming 400 bytes each in a chunk that has 506 bytes of
// payload area, of which four go to the size varints.
uint8_t* chunk = ring->chunk_at(ChunkIndex::FromIndex(0));
uint8_t* sizes_begin = chunk + 512;
sizes_begin = WriteFragmentSizeReversed(sizes_begin, 400);
WriteFragmentSizeReversed(sizes_begin, 400);
Internals::SetChunkStateWord(
ring.get(), ChunkIndex::FromIndex(0),
MakeDataStateWord(ChunkState::kComplete, ChunkFormat::kTargetBuffer, 0, 2,
kWriterB));
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kPositionSkipped);
EXPECT_EQ(reader.GetStats().malformed_chunks, 1u);
EXPECT_TRUE(delegate.chunks.empty());
EXPECT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)),
MakeFreeStateWord(1));
}
TEST(SharedRingBufferReaderTest, UnterminatedFragmentSize) {
test::SharedRingBufferForTesting ring(4, 512);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 0u);
for (uint32_t i = 0; i < kMaxFragmentSizeVarIntBytes; ++i)
ring->chunk_at(ChunkIndex::FromIndex(0))[511 - i] = 0x80;
Internals::SetChunkStateWord(
ring.get(), ChunkIndex::FromIndex(0),
MakeDataStateWord(ChunkState::kComplete, ChunkFormat::kTargetBuffer, 0, 1,
kWriterB));
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kPositionSkipped);
EXPECT_EQ(reader.GetStats().malformed_chunks, 1u);
EXPECT_TRUE(delegate.chunks.empty());
EXPECT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)),
MakeFreeStateWord(1));
}
// A count larger than the writer actually wrote is a producer claim like any
// other. It cannot make the reader leave the chunk or spin. It just decodes the
// stale bytes underneath as extra fragments.
TEST(SharedRingBufferReaderTest, InflatedFragmentCount) {
test::SharedRingBufferForTesting ring(4, 512);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 0u);
Internals::SetChunkStateWord(
ring.get(), ChunkIndex::FromIndex(0),
MakeDataStateWord(ChunkState::kComplete, ChunkFormat::kTargetBuffer, 0, 8,
kWriterB));
EXPECT_EQ(Internals::ConsumeNextPosition(&reader), ConsumeResult::kChunkRead);
// Over a zero-filled mapping the inflated count decodes as extra zero-length
// fragments. The checks make the walk memory-safe. They do not pretend to
// make its contents true.
ASSERT_EQ(delegate.chunks.size(), 1u);
EXPECT_EQ(delegate.chunks[0].fragments.size(), 8u);
for (const std::string& fragment : delegate.chunks[0].fragments)
EXPECT_TRUE(fragment.empty());
EXPECT_EQ(reader.GetStats().malformed_chunks, 0u);
EXPECT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)),
MakeFreeStateWord(1));
EXPECT_EQ(reader.read_pos(), 1u);
}
TEST(SharedRingBufferReaderTest, MalformedBeingWrittenChunk) {
// A malformed BeingWritten chunk is still marked RewriteRequested.
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 0u);
Internals::SetChunkStateWord(
ring.get(), ChunkIndex::FromIndex(0),
MakeDataStateWord(ChunkState::kBeingWritten, ChunkFormat::kTargetBuffer,
0, 255, kWriterB));
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kPositionSkipped);
EXPECT_EQ(reader.GetStats().malformed_chunks, 1u);
// Malformed bytes may be dropped. What must not happen is leaving a
// BeingWritten owner able to publish behind the reader.
EXPECT_EQ(
ChunkStateOf(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0))),
ChunkState::kRewriteRequested);
}
TEST(SharedRingBufferReaderTest, ReservedStateStopsRing) {
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 0u);
const uint32_t reserved_word = 0x00000005u;
Internals::SetChunkStateWord(ring.get(), ChunkIndex::FromIndex(0),
reserved_word);
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kProtocolError);
EXPECT_TRUE(reader.has_protocol_error());
EXPECT_EQ(reader.read_pos(), 0u);
EXPECT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)),
reserved_word);
// The ring buffer is never read again, and a drain over it changes nothing.
const SharedRingBufferReader::DrainResult result = reader.Drain(8);
EXPECT_EQ(result.positions_consumed, 0u);
EXPECT_EQ(result.last_result, ConsumeResult::kProtocolError);
EXPECT_EQ(Internals::GetReadPos(ring.get()), 0u);
}
TEST(SharedRingBufferReaderTest, ForeignFreeWordStopsRing) {
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 0u);
// Position 0 expects Free(0). Only the reader writes a free word, and
// it derives it from the position it is consuming, so no legal execution puts
// this here.
const uint32_t wrong_wrap = MakeFreeStateWord(3);
Internals::SetChunkStateWord(ring.get(), ChunkIndex::FromIndex(0),
wrong_wrap);
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kProtocolError);
EXPECT_TRUE(reader.has_protocol_error());
EXPECT_EQ(reader.read_pos(), 0u);
EXPECT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)),
wrong_wrap);
}
// The control and fragment-count fields of a Free word are zero. A word using
// them may be malformed or belong to an unknown ABI. The reader must stop
// without reclaiming a word it does not understand.
TEST(SharedRingBufferReaderTest, FreeReservedBitsStopRing) {
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 0u);
// The wrap count is position 0's, but a reserved bit is set.
const uint32_t reserved_bit_word = MakeFreeStateWord(0) | (1u << 8);
Internals::SetChunkStateWord(ring.get(), ChunkIndex::FromIndex(0),
reserved_bit_word);
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kProtocolError);
EXPECT_TRUE(reader.has_protocol_error());
EXPECT_EQ(reader.read_pos(), 0u);
EXPECT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)),
reserved_bit_word);
}
// RewriteAcknowledged has one canonical word: the state bits and nothing else.
// A word with those state bits plus payload bits is not one a legal writer can
// leave behind, so the reader stops on it instead of reclaiming the chunk, and
// the forged word stays where it was.
TEST(SharedRingBufferReaderTest, ForgedRewriteAckStopsRing) {
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
ASSERT_EQ(ring->TryReserveWritePos().chunk_pos, 0u);
const uint32_t forged = kRewriteAcknowledgedStateWord | kFlagDataLoss;
Internals::SetChunkStateWord(ring.get(), ChunkIndex::FromIndex(0), forged);
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kProtocolError);
EXPECT_TRUE(reader.has_protocol_error());
EXPECT_EQ(reader.read_pos(), 0u);
EXPECT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)), forged);
}
// A writer reserves a position only after seeing fewer than num_chunks
// outstanding, so write_pos further ahead than that cannot come from a legal
// producer. Once the mapping is writable by another process, believing it would
// mean consuming positions that were never reserved, one drain pass after
// another, for as long as the producer keeps write_pos there.
TEST(SharedRingBufferReaderTest, TooManyOutstandingStopsRing) {
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
// Four chunks, five outstanding positions.
Internals::SetWritePos(ring.get(), 5);
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kProtocolError);
EXPECT_TRUE(reader.has_protocol_error());
EXPECT_EQ(reader.read_pos(), 0u);
EXPECT_EQ(Internals::GetReadPos(ring.get()), 0u);
EXPECT_TRUE(delegate.chunks.empty());
}
// The boundary itself is legal and must still be drained: num_chunks
// outstanding positions is exactly a full ring buffer.
TEST(SharedRingBufferReaderTest, FullRingIsLegal) {
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
SharedRingBufferWriter writer = MakeWriter(ring.get(), kWriterA, kBuffer);
for (uint32_t i = 0; i < 4; ++i) {
ASSERT_TRUE(WriteFragment(&writer, "x"));
writer.FinishCurrentChunk();
}
ASSERT_EQ(ring->LoadWritePosRelaxed(), 4u);
const SharedRingBufferReader::DrainResult result = reader.Drain(8);
EXPECT_FALSE(reader.has_protocol_error());
EXPECT_EQ(result.positions_consumed, 4u);
EXPECT_EQ(delegate.chunks.size(), 4u);
}
// ---------------------------------------------------------------------------
// Flags and routing survive the trip.
// ---------------------------------------------------------------------------
TEST(SharedRingBufferReaderTest, FlagsAndTargetBufferDelivered) {
test::SharedRingBufferForTesting ring(8, 512);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
SharedRingBufferWriter first = MakeWriter(ring.get(), kWriterA, 11);
ASSERT_TRUE(WriteFragment(&first, "head", /*continues_from_prev=*/false,
/*continues_on_next=*/true));
ASSERT_TRUE(WriteFragment(&first, "tail", /*continues_from_prev=*/true,
/*continues_on_next=*/false));
first.FinishCurrentChunk();
SharedRingBufferWriter second = MakeWriter(ring.get(), kWriterB, 22);
ASSERT_TRUE(WriteFragment(&second, "other"));
second.FinishCurrentChunk();
reader.Drain(16);
ASSERT_EQ(delegate.chunks.size(), 3u);
EXPECT_EQ(delegate.chunks[0].target_buffer, 11);
EXPECT_EQ(delegate.chunks[0].payload_flags, kFlagContinuesOnNextChunk);
EXPECT_EQ(delegate.chunks[1].target_buffer, 11);
EXPECT_EQ(delegate.chunks[1].payload_flags, kFlagContinuesFromPrevChunk);
EXPECT_EQ(delegate.chunks[2].target_buffer, 22);
EXPECT_EQ(delegate.chunks[2].writer_id, kWriterB);
EXPECT_EQ(delegate.chunks[2].payload_flags, 0u);
}
TEST(SharedRingBufferReaderTest, DiscardsAllPublishedFragmentsAfterLoss) {
for (bool writer_is_appending : {false, true}) {
SCOPED_TRACE(writer_is_appending);
test::SharedRingBufferForTesting ring(4, 512);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
SharedRingBufferWriter writer = MakeWriter(ring.get(), kWriterA, kBuffer);
ASSERT_TRUE(WriteFragment(&writer, "before"));
writer.RecordDataLoss();
ASSERT_TRUE(WriteFragment(&writer, "after"));
ASSERT_EQ(ring->LoadWritePosRelaxed(), 1u);
if (writer_is_appending) {
const auto range = writer.BeginFragment(5, false);
ASSERT_EQ(range.result, BeginFragmentResult::kSuccess);
memcpy(range.begin, "clean", 5);
} else {
ASSERT_EQ(writer.FinishCurrentChunk(), EndFragmentResult::kSuccess);
}
const auto result = reader.Drain(1);
EXPECT_EQ(result.positions_consumed, 1u);
EXPECT_EQ(result.last_result, ConsumeResult::kPositionSkipped);
EXPECT_TRUE(delegate.chunks.empty());
EXPECT_EQ(delegate.writers_with_data_loss, std::vector<WriterID>{kWriterA});
EXPECT_EQ(reader.GetStats().data_loss_chunks, 1u);
if (writer_is_appending) {
const uint32_t word =
ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0));
EXPECT_EQ(ChunkStateOf(word), ChunkState::kRewriteRequested);
EXPECT_EQ(NumFragmentsOf(word), 2u);
// The loss has been reported. The unpublished fragment can move to a
// clean chunk without repeating it.
ASSERT_EQ(writer.EndFragment(5, false), EndFragmentResult::kSuccess);
EXPECT_EQ(writer.GetStats().relocations, 1u);
} else {
ASSERT_TRUE(WriteFragment(&writer, "clean"));
}
ASSERT_EQ(writer.FinishCurrentChunk(), EndFragmentResult::kSuccess);
reader.Drain(4);
EXPECT_EQ(delegate.AllFragments(), std::vector<std::string>{"clean"});
EXPECT_EQ(delegate.writers_with_data_loss.size(), 1u);
EXPECT_FALSE(reader.has_protocol_error());
}
}
TEST(SharedRingBufferReaderTest, PendingLossSurvivesReaderWinningPublication) {
test::SharedRingBufferForTesting ring(4, 512);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
SharedRingBufferWriter writer = MakeWriter(ring.get(), kWriterA, kBuffer);
ASSERT_TRUE(WriteFragment(&writer, "before"));
writer.RecordDataLoss();
const auto range = writer.BeginFragment(5, false);
ASSERT_EQ(range.result, BeginFragmentResult::kSuccess);
memcpy(range.begin, "after", 5);
// The new fragment and loss flag are still unpublished. The reader may
// deliver the older fragment, then request a rewrite.
EXPECT_EQ(reader.Drain(1).last_result, ConsumeResult::kChunkRead);
EXPECT_EQ(delegate.AllFragments(), std::vector<std::string>{"before"});
EXPECT_TRUE(delegate.writers_with_data_loss.empty());
ASSERT_EQ(writer.EndFragment(5, false), EndFragmentResult::kSuccess);
EXPECT_EQ(writer.GetStats().relocations, 1u);
EXPECT_NE(
ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(1)) & kFlagDataLoss,
0u);
EXPECT_EQ(reader.Drain(1).last_result, ConsumeResult::kPositionSkipped);
EXPECT_EQ(delegate.AllFragments(), std::vector<std::string>{"before"});
EXPECT_EQ(delegate.writers_with_data_loss, std::vector<WriterID>{kWriterA});
ASSERT_TRUE(WriteFragment(&writer, "clean"));
ASSERT_EQ(writer.FinishCurrentChunk(), EndFragmentResult::kSuccess);
reader.Drain(4);
EXPECT_EQ(delegate.AllFragments(),
(std::vector<std::string>{"before", "clean"}));
EXPECT_EQ(delegate.writers_with_data_loss.size(), 1u);
}
// A writer that recorded a loss, claimed a chunk for its next packet and then
// gave up on it publishes Complete(0) with kFlagDataLoss. Once the reader
// reclaims that chunk no writer can carry the flag any further, so the reader
// reports it itself. The writer's next chunk starts clean, so the loss is
// reported exactly once.
TEST(SharedRingBufferReaderTest, DataLossOnEmptyCompleteChunk) {
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
SharedRingBufferWriter writer = MakeWriter(ring.get(), kWriterA, kBuffer);
writer.RecordDataLoss();
ASSERT_EQ(writer.BeginFragment(4, false).result,
BeginFragmentResult::kSuccess);
ASSERT_EQ(writer.FinishCurrentChunk(), EndFragmentResult::kSuccess);
ASSERT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)),
MakeDataStateWord(ChunkState::kComplete, ChunkFormat::kTargetBuffer,
kFlagDataLoss, 0, kWriterA));
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kPositionSkipped);
EXPECT_TRUE(delegate.chunks.empty());
EXPECT_EQ(delegate.writers_with_data_loss, (std::vector<WriterID>{kWriterA}));
EXPECT_EQ(reader.GetStats().data_loss_chunks, 1u);
EXPECT_EQ(reader.GetStats().positions_skipped, 1u);
EXPECT_EQ(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0)),
MakeFreeStateWord(1));
// The writer's reuse of that chunk fails and its replacement does not repeat
// the flag.
ASSERT_TRUE(WriteFragment(&writer, "after"));
writer.FinishCurrentChunk();
reader.Drain(8);
ASSERT_EQ(delegate.chunks.size(), 1u);
EXPECT_EQ(delegate.chunks[0].payload_flags, 0u);
EXPECT_EQ(delegate.writers_with_data_loss.size(), 1u);
}
// The reader wins against BeingWritten(0) with kFlagDataLoss. It takes no
// fragments and reports no loss: the writer still owns the chunk and moves the
// flag to the relocated fragment, so the loss surfaces once, on that chunk.
TEST(SharedRingBufferReaderTest, DataLossFollowsRelocatedFragment) {
test::SharedRingBufferForTesting ring(4, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
SharedRingBufferWriter writer = MakeWriter(ring.get(), kWriterA, kBuffer);
writer.RecordDataLoss();
const auto range = writer.BeginFragment(6, false);
ASSERT_EQ(range.result, BeginFragmentResult::kSuccess);
memcpy(range.begin, "suffix", 6);
EXPECT_EQ(Internals::ConsumeNextPosition(&reader),
ConsumeResult::kPositionSkipped);
EXPECT_TRUE(delegate.chunks.empty());
EXPECT_TRUE(delegate.writers_with_data_loss.empty());
EXPECT_EQ(
ChunkStateOf(ring->LoadChunkStateWordAcquire(ChunkIndex::FromIndex(0))),
ChunkState::kRewriteRequested);
ASSERT_EQ(writer.EndFragment(6, false), EndFragmentResult::kSuccess);
EXPECT_EQ(writer.GetStats().relocations, 1u);
writer.FinishCurrentChunk();
// Position 0 was the scraped chunk. Position 1 holds the relocated fragment.
reader.Drain(8);
EXPECT_TRUE(delegate.chunks.empty());
EXPECT_EQ(delegate.writers_with_data_loss, (std::vector<WriterID>{kWriterA}));
}
// The smallest supported ring buffer reuses both chunks across traversals.
TEST(SharedRingBufferReaderTest, TwoChunkRing) {
test::SharedRingBufferForTesting ring(2, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
SharedRingBufferWriter writer = MakeWriter(ring.get(), kWriterA, kBuffer);
std::vector<std::string> expected;
for (uint32_t i = 0; i < 5; ++i) {
const std::string bytes = "packet-" + std::to_string(i);
ASSERT_TRUE(WriteFragment(&writer, bytes)) << i;
writer.FinishCurrentChunk();
expected.push_back(bytes);
const SharedRingBufferReader::DrainResult result = reader.Drain(4);
EXPECT_EQ(result.positions_consumed, 1u) << i;
}
EXPECT_EQ(delegate.AllFragments(), expected);
EXPECT_EQ(Internals::ConsumeNextPosition(&reader), ConsumeResult::kNoData);
}
// An aligned, non-power-of-two chunk size, end to end through writer and
// reader.
TEST(SharedRingBufferReaderTest, NonPowerOfTwoChunkSize) {
test::SharedRingBufferForTesting ring(4, 260);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
SharedRingBufferWriter writer = MakeWriter(ring.get(), kWriterA, kBuffer);
// 300 bytes does not fit a 260-byte chunk, so it arrives as two fragments in
// two chunks - the split the upper layer would mark with the continuation
// flags.
const std::string head(252, 'h');
const std::string tail(48, 't');
ASSERT_TRUE(WriteFragment(&writer, head, /*continues_from_prev=*/false,
/*continues_on_next=*/true));
ASSERT_TRUE(WriteFragment(&writer, tail, /*continues_from_prev=*/true,
/*continues_on_next=*/false));
writer.FinishCurrentChunk();
reader.Drain(8);
ASSERT_EQ(delegate.chunks.size(), 2u);
EXPECT_EQ(delegate.chunks[0].payload_flags, kFlagContinuesOnNextChunk);
EXPECT_EQ(delegate.chunks[1].payload_flags, kFlagContinuesFromPrevChunk);
EXPECT_EQ(delegate.AllFragments(), (std::vector<std::string>{head, tail}));
}
TEST(SharedRingBufferReaderTest, ReservationOrderAcrossWriters) {
test::SharedRingBufferForTesting ring(8, 256);
RecordingDelegate delegate;
SharedRingBufferReader reader(ring.get(), &delegate);
// Interleave two writers so their chunks land at alternating positions.
SharedRingBufferWriter a = MakeWriter(ring.get(), kWriterA, kBuffer);
SharedRingBufferWriter b = MakeWriter(ring.get(), kWriterB, kBuffer);
std::vector<std::string> expected;
for (uint32_t i = 0; i < 4; ++i) {
// Each fragment fills its chunk, so every write consumes a new position.
const std::string from_a = "a" + std::to_string(i) + std::string(240, '.');
const std::string from_b = "b" + std::to_string(i) + std::string(240, '.');
ASSERT_TRUE(WriteFragment(&a, from_a));
ASSERT_TRUE(WriteFragment(&b, from_b));
expected.push_back(from_a);
expected.push_back(from_b);
reader.Drain(16);
}
a.FinishCurrentChunk();
b.FinishCurrentChunk();
reader.Drain(16);
EXPECT_EQ(delegate.AllFragments(), expected);
}
} // namespace
} // namespace perfetto::tracing_v2