| /* |
| * Copyright (C) 2025 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 <string.h> |
| |
| #include <initializer_list> |
| #include <random> |
| #include <sstream> |
| #include <vector> |
| |
| #include "perfetto/ext/base/utils.h" |
| #include "perfetto/ext/tracing/core/basic_types.h" |
| #include "perfetto/ext/tracing/core/client_identity.h" |
| #include "perfetto/ext/tracing/core/shared_memory_abi.h" |
| #include "perfetto/ext/tracing/core/trace_packet.h" |
| #include "perfetto/protozero/proto_utils.h" |
| #include "protos/perfetto/trace/trace_packet.pbzero.h" |
| #include "src/base/test/vm_test_utils.h" |
| #include "src/tracing/service/trace_buffer_v2.h" |
| #include "src/tracing/test/fake_packet.h" |
| #include "test/gtest_and_gmock.h" |
| |
| namespace perfetto { |
| |
| using DataLossReason = protos::pbzero::TracePacket_DataLossReason; |
| using ::testing::ContainerEq; |
| using ::testing::ElementsAre; |
| using ::testing::IsEmpty; |
| |
| class TraceBufferV2Test : public testing::Test { |
| public: |
| static constexpr uint8_t kContFromPrevChunk = |
| SharedMemoryABI::ChunkHeader::kFirstPacketContinuesFromPrevChunk; |
| static constexpr uint8_t kContOnNextChunk = |
| SharedMemoryABI::ChunkHeader::kLastPacketContinuesOnNextChunk; |
| static constexpr uint8_t kChunkNeedsPatching = |
| SharedMemoryABI::ChunkHeader::kChunkNeedsPatching; |
| |
| void TearDown() override { |
| // Test that the used_size() logic works and that all the data after that |
| // is zero-filled. |
| if (trace_buffer_) { |
| const size_t used_size = trace_buffer_->used_size(); |
| ASSERT_LE(used_size, trace_buffer_->size()); |
| trace_buffer()->data_.EnsureCommitted(trace_buffer_->size()); |
| bool zero_padded = true; |
| for (size_t i = used_size; i < trace_buffer_->size(); ++i) { |
| bool is_zero = static_cast<char*>(trace_buffer()->data_.Get())[i] == 0; |
| zero_padded = zero_padded && is_zero; |
| } |
| ASSERT_TRUE(zero_padded); |
| } |
| } |
| |
| FakeChunk CreateChunk(ProducerID p, WriterID w, ChunkID c) { |
| return FakeChunk(trace_buffer_.get(), p, w, c); |
| } |
| |
| void ResetBuffer( |
| size_t size_, |
| TraceBuffer::OverwritePolicy policy = TraceBuffer::kOverwrite) { |
| trace_buffer_ = TraceBufferV2::Create(size_, policy); |
| ASSERT_TRUE(trace_buffer_); |
| } |
| |
| bool TryPatchChunkContents(ProducerID p, |
| WriterID w, |
| ChunkID c, |
| std::vector<TraceBuffer::Patch> patches, |
| bool other_patches_pending = false) { |
| return trace_buffer_->TryPatchChunkContents( |
| p, w, c, patches.data(), patches.size(), other_patches_pending); |
| } |
| |
| static std::vector<FakePacketFragment> ReadPacket( |
| const std::unique_ptr<TraceBuffer>& buf, |
| TraceBuffer::PacketSequenceProperties* sequence_properties = nullptr, |
| uint32_t* previous_packet_dropped = nullptr) { |
| std::vector<FakePacketFragment> fragments; |
| TracePacket packet; |
| TraceBuffer::PacketSequenceProperties ignored_sequence_properties{}; |
| uint32_t ignored_previous_packet_dropped; |
| if (!buf->ReadNextTracePacket( |
| &packet, |
| sequence_properties ? sequence_properties |
| : &ignored_sequence_properties, |
| previous_packet_dropped ? previous_packet_dropped |
| : &ignored_previous_packet_dropped)) { |
| return fragments; |
| } |
| for (const Slice& slice : packet.slices()) |
| fragments.emplace_back(slice.start, slice.size); |
| return fragments; |
| } |
| |
| std::vector<FakePacketFragment> ReadPacket( |
| TraceBuffer::PacketSequenceProperties* sequence_properties = nullptr, |
| uint32_t* previous_packet_dropped = nullptr) { |
| return ReadPacket(trace_buffer_, sequence_properties, |
| previous_packet_dropped); |
| } |
| |
| void AppendChunks( |
| std::initializer_list<std::tuple<ProducerID, WriterID, ChunkID>> chunks) { |
| for (const auto& c : chunks) { |
| char seed = |
| static_cast<char>(std::get<0>(c) + std::get<1>(c) + std::get<2>(c)); |
| CreateChunk(std::get<0>(c), std::get<1>(c), std::get<2>(c)) |
| .AddPacket(4, seed) |
| .CopyIntoTraceBuffer(); |
| } |
| } |
| |
| void SuppressClientDchecksForTesting() { |
| trace_buffer()->suppress_client_dchecks_for_testing_ = true; |
| } |
| |
| uint8_t* GetBufData(const TraceBuffer& buf) { |
| return static_cast<const TraceBufferV2&>(buf).begin(); |
| } |
| |
| size_t size_to_end() { return trace_buffer()->size_to_end(); } |
| |
| TraceBufferV2* trace_buffer() { |
| return static_cast<TraceBufferV2*>(trace_buffer_.get()); |
| } |
| |
| protected: |
| std::unique_ptr<TraceBuffer> trace_buffer_; |
| }; |
| |
| // ---------------------- |
| // Main TraceBufferV2 tests |
| // ---------------------- |
| |
| // Note for the test code: remember that the resulting size of a chunk is: |
| // SUM(packets) + 16 (that is sizeof(ChunkRecord)). |
| // Also remember that chunks are rounded up to 16. So, unless we are testing the |
| // rounding logic, might be a good idea to create chunks of that size. |
| |
| TEST_F(TraceBufferV2Test, ReadWrite_EmptyBuffer) { |
| ResetBuffer(4096); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // On each iteration writes a fixed-size chunk and reads it back. |
| TEST_F(TraceBufferV2Test, ReadWrite_Simple) { |
| ResetBuffer(64 * 1024); |
| for (ChunkID chunk_id = 0; chunk_id < 1000; chunk_id++) { |
| char seed = static_cast<char>(chunk_id); |
| CreateChunk(ProducerID(1), WriterID(1), chunk_id) |
| .AddPacket(42, seed) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(42, seed))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| EXPECT_EQ(chunk_id + 1u, trace_buffer()->stats().chunks_written()); |
| EXPECT_EQ(trace_buffer()->stats().chunks_written(), |
| trace_buffer()->stats().chunks_read()); |
| EXPECT_LT(0u, trace_buffer()->stats().bytes_written()); |
| EXPECT_EQ(trace_buffer()->stats().bytes_written(), |
| trace_buffer()->stats().bytes_read()); |
| EXPECT_EQ(0u, trace_buffer()->stats().padding_bytes_written()); |
| EXPECT_EQ(0u, trace_buffer()->stats().padding_bytes_cleared()); |
| } |
| } |
| |
| TEST_F(TraceBufferV2Test, ReadWrite_OneChunkPerWriter) { |
| for (int8_t num_writers = 1; num_writers <= 10; num_writers++) { |
| ResetBuffer(4096); |
| for (char i = 1; i <= num_writers; i++) { |
| ASSERT_EQ(32u, CreateChunk(ProducerID(i), WriterID(i), ChunkID(i)) |
| .AddPacket(32 - 16, i) |
| .CopyIntoTraceBuffer()); |
| } |
| |
| // The expected read sequence now is: c3, c4, c5. |
| trace_buffer()->BeginRead(); |
| for (char i = 1; i <= num_writers; i++) |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(32 - 16, i))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } // for(num_writers) |
| } |
| |
| // Writes chunk that up filling the buffer precisely until the end, like this: |
| // [ c0: 512 ][ c1: 512 ][ c2: 1024 ][ c3: 2048 ] |
| // | ---------------- 4k buffer --------------- | |
| TEST_F(TraceBufferV2Test, ReadWrite_FillTillEnd) { |
| ResetBuffer(4096); |
| for (int i = 0; i < 3; i++) { |
| ASSERT_EQ(512u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(i * 4)) |
| .AddPacket(512 - 16, 'a') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(512u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(i * 4 + 1)) |
| .AddPacket(512 - 16, 'b') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(1024u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(i * 4 + 2)) |
| .AddPacket(1024 - 16, 'c') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(2048u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(i * 4 + 3)) |
| .AddPacket(2048 - 16, 'd') |
| .CopyIntoTraceBuffer()); |
| |
| // The write pointer lands exactly at the end of the buffer. |
| ASSERT_EQ(0u, size_to_end()); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(512 - 16, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(512 - 16, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(1024 - 16, 'c'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(2048 - 16, 'd'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| } |
| |
| // Similar to the above, but this time leaves some gap at the end and then |
| // tries to add a chunk that doesn't fit to exercise the padding-at-end logic. |
| // Initial condition: |
| // [ c0: 128 ][ c1: 256 ][ c2: 512 ][ c3: 1024 ][ c4: 2048 ]{ 128 padding } |
| // | ------------------------------- 4k buffer ------------------------------ | |
| // |
| // At this point we try to insert a 512 Bytes chunk (c5). The result should be: |
| // [ c5: 512 ]{ padding }[c3: 1024 ][ c4: 2048 ]{ 128 padding } |
| // | ------------------------------- 4k buffer ------------------------------ | |
| TEST_F(TraceBufferV2Test, ReadWrite_Padding) { |
| ResetBuffer(4096); |
| ASSERT_EQ(128u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(128 - 16, 'a') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(256u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(256 - 16, 'b') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(512u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(512 - 16, 'c') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(1024u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(3)) |
| .AddPacket(1024 - 16, 'd') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(2048u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(4)) |
| .AddPacket(2048 - 16, 'e') |
| .CopyIntoTraceBuffer()); |
| |
| // Now write c5 that will cause wrapping + padding. |
| ASSERT_EQ(128u, size_to_end()); |
| ASSERT_EQ(512u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(5)) |
| .AddPacket(512 - 16, 'f') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(4096u - 512, size_to_end()); |
| |
| // The expected read sequence now is: c3, c4, c5. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(1024 - 16, 'd'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(2048 - 16, 'e'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(512 - 16, 'f'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| EXPECT_EQ(6u, trace_buffer()->stats().chunks_written()); |
| EXPECT_EQ(3u, trace_buffer()->stats().chunks_overwritten()); |
| EXPECT_EQ(3u, trace_buffer()->stats().chunks_read()); |
| EXPECT_EQ(4480u, trace_buffer()->stats().bytes_written()); |
| EXPECT_EQ(896u, trace_buffer()->stats().bytes_overwritten()); |
| EXPECT_EQ(3584u, trace_buffer()->stats().bytes_read()); |
| EXPECT_EQ(384u, trace_buffer()->stats().padding_bytes_written()); |
| EXPECT_EQ(0u, trace_buffer()->stats().padding_bytes_cleared()); |
| |
| // Adding another chunk should clear some of the padding. |
| ASSERT_EQ(128u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(6)) |
| .AddPacket(128 - 16, 'g') |
| .CopyIntoTraceBuffer()); |
| EXPECT_EQ(384u, trace_buffer()->stats().padding_bytes_cleared()); |
| } |
| |
| // Like ReadWrite_Padding, but this time the padding introduced is the minimum |
| // allowed (16 bytes). This is to exercise edge cases in the padding logic. |
| // [c0: 2048 ][c1: 1024 ][c2: 1008 ][c3: 16] |
| // [c4: 2032 ][c5: 1040 ][c6 :16][c7: 1080 ] |
| TEST_F(TraceBufferV2Test, ReadWrite_MinimalPadding) { |
| ResetBuffer(4096); |
| |
| ASSERT_EQ(2048u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(2048 - 16, 'a') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(1024u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(1024 - 16, 'b') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(1008u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(1008 - 16, 'c') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(16u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(3)) |
| .CopyIntoTraceBuffer()); |
| |
| ASSERT_EQ(0u, size_to_end()); |
| |
| ASSERT_EQ(2032u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(4)) |
| .AddPacket(2032 - 16, 'd') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(1040u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(5)) |
| .AddPacket(1040 - 16, 'e') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(16u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(6)) |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(1008u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(7)) |
| .AddPacket(1008 - 16, 'f') |
| .CopyIntoTraceBuffer()); |
| |
| ASSERT_EQ(0u, size_to_end()); |
| |
| // The expected read sequence now is: c3, c4, c5. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(2032 - 16, 'd'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(1040 - 16, 'e'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(1008 - 16, 'f'))); |
| for (int i = 0; i < 3; i++) |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // NOTE: I had to change this test from V1 because it was assuming readback in |
| // producer,writer order, while instead now expects buffer order. |
| TEST_F(TraceBufferV2Test, ReadWrite_RandomChunksNoWrapping) { |
| for (unsigned int seed = 1; seed <= 32; seed++) { |
| std::minstd_rand0 rnd_engine(seed); |
| ResetBuffer(4096 * (1 + rnd_engine() % 32)); |
| std::uniform_int_distribution<size_t> size_dist(18, 4096); |
| std::uniform_int_distribution<ProducerID> prod_dist(1, kMaxProducerID); |
| std::uniform_int_distribution<WriterID> wri_dist(1, kMaxWriterID); |
| ChunkID chunk_id = 0; |
| std::vector<std::tuple<ProducerID, WriterID, ChunkID, size_t>> expected; |
| for (;;) { |
| const size_t chunk_size = size_dist(rnd_engine); |
| if (base::AlignUp<16>(chunk_size) >= size_to_end()) |
| break; |
| ProducerID p = prod_dist(rnd_engine); |
| WriterID w = wri_dist(rnd_engine); |
| ChunkID c = chunk_id++; |
| expected.emplace_back(std::make_tuple(p, w, c, chunk_size)); |
| ASSERT_EQ(chunk_size, |
| CreateChunk(p, w, c) |
| .AddPacket(chunk_size - 16, static_cast<char>(chunk_size)) |
| .CopyIntoTraceBuffer()); |
| } // for(;;) |
| trace_buffer()->BeginRead(); |
| for (const auto& it : expected) { |
| const size_t chunk_size = std::get<3>(it); |
| ASSERT_THAT(ReadPacket(), |
| ElementsAre(FakePacketFragment( |
| chunk_size - 16, static_cast<char>(chunk_size)))); |
| } |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| } |
| |
| // Tests the case of writing a chunk that leaves just sizeof(ChunkRecord) at |
| // the end of the buffer. |
| TEST_F(TraceBufferV2Test, ReadWrite_WrappingCases) { |
| ResetBuffer(4096); |
| ASSERT_EQ(4080u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(4080 - 16, 'a') |
| .CopyIntoTraceBuffer()); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(4080 - 16, 'a'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| ASSERT_EQ(16u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(2048u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(2048 - 16, 'b') |
| .CopyIntoTraceBuffer()); |
| |
| ASSERT_EQ(2048u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(3)) |
| .AddPacket(2048 - 16, 'c') |
| .CopyIntoTraceBuffer()); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(2048 - 16, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(2048 - 16, 'c'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Verify that empty packets are skipped. |
| TEST_F(TraceBufferV2Test, ReadWrite_EmptyPacket) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), 0) |
| .AddPacket(42, 1) |
| .AddPacket(1, 2) |
| .AddPacket(42, 3) |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(42, 1))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(42, 3))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| EXPECT_EQ(0u, trace_buffer()->stats().abi_violations()); |
| } |
| |
| // -------------------------------------- |
| // Fragments stitching and skipping logic |
| // -------------------------------------- |
| |
| TEST_F(TraceBufferV2Test, Fragments_Simple) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(10, 'a', kContFromPrevChunk) |
| .AddPacket(20, 'b') |
| .AddPacket(30, 'c') |
| .AddPacket(10, 'd', kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(20, 'e', kContFromPrevChunk) |
| .AddPacket(30, 'f') |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| // The (10, 'a') entry should be skipped because we don't have provided the |
| // previous chunk, hence should be treated as a data loss. |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'c'))); |
| |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'd'), |
| FakePacketFragment(20, 'e'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'f'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Fragments_EdgeCases) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(2, 'a', kContFromPrevChunk) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(2, 'b', kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Now add the missing fragment. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(2, 'c', kContFromPrevChunk) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(2, 'b'), |
| FakePacketFragment(2, 'c'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // The following tests verify that chunks received out-of-order are read in the |
| // correct order. |
| // |
| // Fragment order {0,2,1} for sequence {1,1}, without fragmenting packets. |
| TEST_F(TraceBufferV2Test, Fragments_OutOfOrderLastChunkIsMiddle) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(10, 'a') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(30, 'c') |
| .CopyIntoTraceBuffer(); |
| EXPECT_EQ(0u, trace_buffer()->stats().chunks_committed_out_of_order()); |
| trace_buffer()->BeginRead(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(20, 'b') |
| .CopyIntoTraceBuffer(); |
| EXPECT_EQ(1u, trace_buffer()->stats().chunks_committed_out_of_order()); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'c'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Fragment order {0,2,1} for sequence {1,1}, with fragmenting packets. |
| TEST_F(TraceBufferV2Test, Fragments_OutOfOrderLastChunkIsMiddleFragmentation) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(10, 'a', kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(30, 'c', kContFromPrevChunk) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(20, 'b', kContFromPrevChunk | kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'a'), |
| FakePacketFragment(20, 'b'), |
| FakePacketFragment(30, 'c'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Fragment order {0,2,1,3} for sequence {1,1}, with fragmenting packets. Also |
| // verifies that another sequence isn't broken. |
| TEST_F(TraceBufferV2Test, Fragments_OutOfOrderLastChunkIsMaxFragmentation) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(10, 'a', kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(30, 'c', kContFromPrevChunk) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(20, 'b', kContFromPrevChunk | kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(3)) |
| .AddPacket(40, 'd') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'a'), |
| FakePacketFragment(20, 'b'), |
| FakePacketFragment(30, 'c'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(40, 'd'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Fragment order {-2,1,-1,0} for sequence {1,1}, without fragmenting packets. |
| TEST_F(TraceBufferV2Test, Fragments_OutOfOrderWithIdOverflowADCB) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(kMaxChunkID - 1)) |
| .AddPacket(10, 'a') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(40, 'd') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'a'))); |
| // ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(30, 'c') |
| .CopyIntoTraceBuffer(); |
| // trace_buffer()->BeginRead(); |
| // ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(kMaxChunkID)) |
| .AddPacket(20, 'b') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'c'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(40, 'd'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Fragment order {-2,0,-1,1} for sequence {1,1}, without fragmenting packets. |
| TEST_F(TraceBufferV2Test, Fragments_OutOfOrderWithIdOverflowACBD) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(kMaxChunkID - 1)) |
| .AddPacket(10, 'a') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(30, 'c') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'a'))); |
| // ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(kMaxChunkID)) |
| .AddPacket(20, 'b') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'c'))); |
| // ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(40, 'd') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(40, 'd'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Fragments_EmptyChunkBefore) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)).CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(10, 'a') |
| .AddPacket(20, 'b', kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(30, 'c', kContFromPrevChunk) |
| .AddPacket(40, 'd', kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'b'), |
| FakePacketFragment(30, 'c'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Fragments_EmptyChunkAfter) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(10, 'a') |
| .AddPacket(10, 'b', kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)).CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'a'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Set up a fragmented packet that happens to also have an empty chunk in the |
| // middle of the sequence. Test that it just gets skipped. |
| TEST_F(TraceBufferV2Test, Fragments_EmptyChunkInTheMiddle) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(10, 'a', kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)).CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(10, 'b', kContFromPrevChunk) |
| .AddPacket(20, 'c') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'a'), |
| FakePacketFragment(10, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'c'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Generates sequences of fragmented packets of increasing length (|seq_len|), |
| // from [P0, P1a][P1y] to [P0, P1a][P1b][P1c]...[P1y]. Test that they are always |
| // read as one packet. |
| TEST_F(TraceBufferV2Test, Fragments_LongPackets) { |
| for (unsigned seq_len = 1; seq_len <= 10; seq_len++) { |
| ResetBuffer(4096); |
| std::vector<FakePacketFragment> expected_fragments; |
| expected_fragments.emplace_back(20, 'b'); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(10, 'a') |
| .AddPacket(20, 'b', kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| for (unsigned i = 1; i <= seq_len; i++) { |
| char prefix = 'b' + static_cast<char>(i); |
| expected_fragments.emplace_back(20 + i, prefix); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(i)) |
| .AddPacket(20 + i, prefix, kContFromPrevChunk | kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| } |
| expected_fragments.emplace_back(30, 'y'); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(seq_len + 1)) |
| .AddPacket(30, 'y', kContFromPrevChunk) |
| .AddPacket(50, 'z') |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'a'))); |
| ASSERT_THAT(ReadPacket(), ContainerEq(expected_fragments)); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(50, 'z'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| } |
| |
| // Similar to Fragments_LongPacket, but covers also the case of ChunkID wrapping |
| // over its max value. |
| TEST_F(TraceBufferV2Test, Fragments_LongPacketWithWrappingID) { |
| ResetBuffer(4096); |
| std::vector<FakePacketFragment> expected_fragments; |
| |
| for (ChunkID chunk_id = static_cast<ChunkID>(-2); chunk_id <= 2; chunk_id++) { |
| char prefix = static_cast<char>('c' + chunk_id); |
| expected_fragments.emplace_back(10 + chunk_id, prefix); |
| CreateChunk(ProducerID(1), WriterID(1), chunk_id) |
| .AddPacket(10 + chunk_id, prefix, kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| } |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ContainerEq(expected_fragments)); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Change from TraceBufferV1: here I had to swap the order of expected packets |
| // because now we respect buffer order rather than going by {producer,writer}. |
| TEST_F(TraceBufferV2Test, Fragments_PreserveUID) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(10, 'a') |
| .AddPacket(10, 'b', kContOnNextChunk) |
| .SetUID(11) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(0)) |
| .AddPacket(10, 'c') |
| .AddPacket(10, 'd') |
| .SetUID(22) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(10, 'e', kContFromPrevChunk) |
| .AddPacket(10, 'f') |
| .SetUID(11) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| TraceBuffer::PacketSequenceProperties sequence_properties; |
| ASSERT_THAT(ReadPacket(&sequence_properties), |
| ElementsAre(FakePacketFragment(10, 'a'))); |
| ASSERT_EQ(static_cast<uid_t>(11), sequence_properties.producer_uid_trusted()); |
| |
| ASSERT_THAT( |
| ReadPacket(&sequence_properties), |
| ElementsAre(FakePacketFragment(10, 'b'), FakePacketFragment(10, 'e'))); |
| ASSERT_EQ(static_cast<uid_t>(11), sequence_properties.producer_uid_trusted()); |
| |
| ASSERT_THAT(ReadPacket(&sequence_properties), |
| ElementsAre(FakePacketFragment(10, 'c'))); |
| ASSERT_EQ(static_cast<uid_t>(22), sequence_properties.producer_uid_trusted()); |
| |
| ASSERT_THAT(ReadPacket(&sequence_properties), |
| ElementsAre(FakePacketFragment(10, 'd'))); |
| ASSERT_EQ(static_cast<uid_t>(22), sequence_properties.producer_uid_trusted()); |
| |
| ASSERT_THAT(ReadPacket(&sequence_properties), |
| ElementsAre(FakePacketFragment(10, 'f'))); |
| ASSERT_EQ(static_cast<uid_t>(11), sequence_properties.producer_uid_trusted()); |
| |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Fragments_DiscardedOnPacketSizeDropPacket) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| // Set up a fragmented packet in the first chunk, which continues in the |
| // second chunk with kPacketSizeDropPacket size. The corrupted fragmented |
| // packet should be skipped. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(10, 'a') |
| .AddPacket(10, 'b', kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .SetFlags(kContFromPrevChunk) |
| // Var-int encoded TraceWriterImpl::kPacketSizeDropPacket. |
| .AddPacket({0xff, 0xff, 0xff, 0x7f}) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(10, 'd') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'd'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Fragments_PacketSizeDropPacketIsNotAbiViolation) { |
| ResetBuffer(4096); |
| // A TraceWriter aborting a packet with kPacketSizeDropPacket is legitimate |
| // behaviour since Android R. It must be accounted as a trace writer data |
| // loss and not as an ABI violation. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(10, 'a') |
| // Var-int encoded TraceWriterImpl::kPacketSizeDropPacket. |
| .AddPacket({0xff, 0xff, 0xff, 0x7f}) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'a'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| EXPECT_EQ(0u, trace_buffer()->stats().abi_violations()); |
| EXPECT_EQ(1u, trace_buffer()->stats().trace_writer_packet_loss()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Fragments_IncompleteChunkNeedsPatching) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b', kContOnNextChunk | kChunkNeedsPatching) |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| trace_buffer()->BeginRead(); |
| // First packet should be read even if the chunk's last packet still needs |
| // patching. |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // -------------------------- |
| // Out of band patching tests |
| // -------------------------- |
| |
| TEST_F(TraceBufferV2Test, Patching_Simple) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(100, 'a') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(0)) |
| .AddPacket(9, 'b') |
| .ClearBytes(5, 4) // 5 := 4th payload byte. Byte 0 is the varint header. |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(3), WriterID(1), ChunkID(0)) |
| .AddPacket(100, 'c') |
| .CopyIntoTraceBuffer(); |
| ASSERT_TRUE(TryPatchChunkContents(ProducerID(2), WriterID(1), ChunkID(0), |
| {{5, {{'Y', 'M', 'C', 'A'}}}})); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(100, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment("b00-YMCA", 8))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(100, 'c'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Patching_SkipIfChunkDoesntExist) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(100, 'a') |
| .CopyIntoTraceBuffer(); |
| ASSERT_FALSE(TryPatchChunkContents(ProducerID(1), WriterID(2), ChunkID(0), |
| {{0, {{'X', 'X', 'X', 'X'}}}})); |
| ASSERT_FALSE(TryPatchChunkContents(ProducerID(1), WriterID(1), ChunkID(1), |
| {{0, {{'X', 'X', 'X', 'X'}}}})); |
| ASSERT_FALSE(TryPatchChunkContents(ProducerID(1), WriterID(1), ChunkID(-1), |
| {{0, {{'X', 'X', 'X', 'X'}}}})); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(100, 'a'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Patching_AtBoundariesOfChunk) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(100, 'a', kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(16, 'b', kContFromPrevChunk | kContOnNextChunk) |
| .ClearBytes(1, 4) |
| .ClearBytes(16 - 4, 4) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(100, 'c', kContFromPrevChunk) |
| .CopyIntoTraceBuffer(); |
| ASSERT_TRUE(TryPatchChunkContents( |
| ProducerID(1), WriterID(1), ChunkID(1), |
| {{1, {{'P', 'E', 'R', 'F'}}}, {16 - 4, {{'E', 'T', 'T', 'O'}}}})); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), |
| ElementsAre(FakePacketFragment(100, 'a'), |
| FakePacketFragment("PERFb01-b02ETTO", 15), |
| FakePacketFragment(100, 'c'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Tests kChunkNeedsPatching logic: chunks that are marked as "pending patch" |
| // should not be read until the patch has happened. |
| TEST_F(TraceBufferV2Test, Patching_ReadWaitsForPatchComplete) { |
| ResetBuffer(4096); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(16, 'a', kChunkNeedsPatching | kContOnNextChunk) |
| .ClearBytes(1, 4) // 1 := 0th payload byte. Byte 0 is the varint header. |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(16, 'b', kContFromPrevChunk) |
| .CopyIntoTraceBuffer(); |
| |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(0)) |
| .AddPacket(16, 'c') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(1)) |
| .AddPacket(16, 'd', kChunkNeedsPatching | kContOnNextChunk) |
| .ClearBytes(1, 4) // 1 := 0th payload byte. Byte 0 is the varint header. |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(2)) |
| .AddPacket(16, 'e', kContFromPrevChunk) |
| .CopyIntoTraceBuffer(); |
| |
| CreateChunk(ProducerID(3), WriterID(1), ChunkID(0)) |
| .AddPacket(16, 'f', kChunkNeedsPatching | kContOnNextChunk) |
| .ClearBytes(1, 8) // 1 := 0th payload byte. Byte 0 is the varint header. |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(3), WriterID(1), ChunkID(1)) |
| .AddPacket(1, '\0', kContFromPrevChunk) |
| .CopyIntoTraceBuffer(); |
| |
| // The only thing that can be read right now is the 1st packet of the 2nd |
| // sequence. All the rest is blocked waiting for patching. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(16, 'c'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Now patch the 2nd sequence and check that the sequence is unblocked. |
| ASSERT_TRUE(TryPatchChunkContents(ProducerID(2), WriterID(1), ChunkID(1), |
| {{1, {{'P', 'A', 'T', 'C'}}}})); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), |
| ElementsAre(FakePacketFragment("PATCd01-d02-d03", 15), |
| FakePacketFragment(16, 'e'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Now patch the 3rd sequence, but in the first patch set |
| // |other_patches_pending| to true, so that the sequence is unblocked only |
| // after the 2nd patch. |
| ASSERT_TRUE(TryPatchChunkContents(ProducerID(3), WriterID(1), ChunkID(0), |
| {{1, {{'P', 'E', 'R', 'F'}}}}, |
| /*other_patches_pending=*/true)); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| ASSERT_TRUE(TryPatchChunkContents(ProducerID(3), WriterID(1), ChunkID(0), |
| {{5, {{'E', 'T', 'T', 'O'}}}}, |
| /*other_patches_pending=*/false)); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), |
| ElementsAre(FakePacketFragment("PERFETTOf02-f03", 15))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Tests that if we have pending patches and those chunks get overwritten, |
| // we still detect data loss properly. |
| TEST_F(TraceBufferV2Test, PendingPatchesDataLossOnOverwrite) { |
| ResetBuffer(4096); |
| |
| // Create a fragmented packet that needs patching |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(1024, 'a', kContOnNextChunk | kChunkNeedsPatching) |
| .CopyIntoTraceBuffer(); |
| |
| // Create the continuation chunk |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(1024, 'b', kContFromPrevChunk) |
| .CopyIntoTraceBuffer(); |
| |
| // Verify the chunk is waiting for patches (can't be read) |
| trace_buffer()->BeginRead(); |
| // Should be empty because chunk needs patching |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Now write large chunks to cause buffer wrap and overwrite the pending |
| // chunks |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(2000, 'c') |
| .CopyIntoTraceBuffer(); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(3)) |
| .AddPacket(2000, 'd') |
| .CopyIntoTraceBuffer(); |
| |
| // The pending chunks should have been overwritten. When we read the next |
| // chunk in the sequence, we should see a data loss because chunks 0-1 |
| // (which were pending patches) were overwritten before being completed. |
| uint32_t previous_packet_dropped = 0; |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(2000, 'c'))); |
| EXPECT_TRUE(previous_packet_dropped); // Data loss should be detected |
| |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(2000, 'd'))); |
| EXPECT_FALSE(previous_packet_dropped); // No data loss for this packet |
| |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // ---------------------------- |
| // Data-loss attribution tests |
| // ---------------------------- |
| |
| // A ChunkID gap between two reads is attributed with the DATA_LOSS_READ_GAP |
| // cause bit. |
| TEST_F(TraceBufferV2Test, DataLoss_ReadGap) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(32, 'a') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(32, 'a'))); |
| |
| // ChunkID 1 never arrives; ChunkID 2 does. Reading it must detect the gap. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(32, 'b') |
| .CopyIntoTraceBuffer(); |
| uint32_t previous_packet_dropped = 0; |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(32, 'b'))); |
| EXPECT_EQ(static_cast<uint32_t>(DataLossReason::DATA_LOSS_PRESENT | |
| DataLossReason::DATA_LOSS_READ_GAP), |
| previous_packet_dropped); |
| } |
| |
| // Overwriting several unread chunks of one sequence before reading collapses |
| // into a single loss: the per-sequence bitmask is set once and surfaces on the |
| // first survivor with the overwrite cause. |
| TEST_F(TraceBufferV2Test, DataLoss_OverwriteVsChunkCount) { |
| ResetBuffer(4096); |
| const auto& stats = trace_buffer()->stats(); |
| // One sequence written until the ring wraps and overwrites its own unread |
| // chunks, all before any read. |
| for (ChunkID c = 0; c < 16; c++) { |
| CreateChunk(ProducerID(1), WriterID(1), c) |
| .AddPacket(400, 'x') |
| .CopyIntoTraceBuffer(); |
| } |
| ASSERT_LE(2u, stats.chunks_overwritten()); // Several chunks lost... |
| |
| trace_buffer()->BeginRead(); |
| uint32_t previous_packet_dropped = 0; |
| bool first = true; |
| while (!ReadPacket(nullptr, &previous_packet_dropped).empty()) { |
| // Only the first survivor is flagged, and with the overwrite cause. |
| EXPECT_EQ(first ? static_cast<uint32_t>(DataLossReason::DATA_LOSS_PRESENT | |
| DataLossReason::DATA_LOSS_OVERWRITE) |
| : 0u, |
| previous_packet_dropped); |
| first = false; |
| } |
| EXPECT_FALSE(first); // At least one survivor was read (loop wasn't vacuous). |
| } |
| |
| // A continuation/end fragment with no preceding begin fragment (the begin chunk |
| // was lost) is attributed with the DATA_LOSS_ORPHAN_CONTINUATION cause bit. |
| TEST_F(TraceBufferV2Test, DataLoss_OrphanContinuation) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(32, 'a', kContFromPrevChunk) // kFragEnd with no begin. |
| .CopyIntoTraceBuffer(); |
| // A later whole packet on the same sequence carries the dropped flag out. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(32, 'b') |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| uint32_t previous_packet_dropped = 0; |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(32, 'b'))); |
| EXPECT_EQ( |
| static_cast<uint32_t>(DataLossReason::DATA_LOSS_PRESENT | |
| DataLossReason::DATA_LOSS_ORPHAN_CONTINUATION), |
| previous_packet_dropped); |
| } |
| |
| // A multi-chunk packet whose chain is broken even though ChunkIDs are |
| // contiguous (the continuation chunk is a whole packet, not a continue/end) is |
| // attributed with the DATA_LOSS_REASSEMBLY_BROKEN_CHAIN cause bit. |
| TEST_F(TraceBufferV2Test, DataLoss_ReassemblyBrokenChain) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(32, 'a', kContOnNextChunk) // begin, expects a continuation. |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(32, 'b') // whole packet, not a continuation: chain broken. |
| .CopyIntoTraceBuffer(); |
| |
| uint32_t previous_packet_dropped = 0; |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(32, 'b'))); |
| EXPECT_EQ( |
| static_cast<uint32_t>(DataLossReason::DATA_LOSS_PRESENT | |
| DataLossReason::DATA_LOSS_REASSEMBLY_BROKEN_CHAIN), |
| previous_packet_dropped); |
| } |
| |
| // A ChunkID gap in the middle of a fragmented packet is attributed with the |
| // DATA_LOSS_REASSEMBLY_GAP cause bit. |
| TEST_F(TraceBufferV2Test, DataLoss_ReassemblyGap) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(32, 'a', kContOnNextChunk) // begin. |
| .CopyIntoTraceBuffer(); |
| // ChunkID 1 (the continuation) is missing; ChunkID 2 is the tail. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(32, 'b', kContFromPrevChunk) // end. |
| .CopyIntoTraceBuffer(); |
| // A later whole packet on the same sequence carries the dropped flag out. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(3)) |
| .AddPacket(32, 'c') |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| uint32_t previous_packet_dropped = 0; |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(32, 'c'))); |
| // The ChunkID 0->2 gap is seen as both a read gap (DATA_LOSS_READ_GAP) and a |
| // reassembly gap (DATA_LOSS_REASSEMBLY_GAP), and the orphaned tail fragment |
| // in ChunkID 2 then surfaces as DATA_LOSS_ORPHAN_CONTINUATION: a single loss |
| // can have multiple causes. |
| EXPECT_EQ( |
| static_cast<uint32_t>(DataLossReason::DATA_LOSS_PRESENT | |
| DataLossReason::DATA_LOSS_READ_GAP | |
| DataLossReason::DATA_LOSS_REASSEMBLY_GAP | |
| DataLossReason::DATA_LOSS_ORPHAN_CONTINUATION), |
| previous_packet_dropped); |
| } |
| |
| // --------------------- |
| // Malicious input tests |
| // --------------------- |
| |
| TEST_F(TraceBufferV2Test, Malicious_ZeroSizedChunk) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(32, 'a') |
| .CopyIntoTraceBuffer(); |
| |
| uint8_t valid_ptr = 0; |
| trace_buffer()->CopyChunkUntrusted( |
| ProducerID(1), ClientIdentity(uid_t(0), pid_t(0)), WriterID(1), |
| ChunkID(1), 1 /* num packets */, 0 /* flags */, true /* chunk_complete */, |
| &valid_ptr, sizeof(valid_ptr)); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(32, 'b') |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(32, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(32, 'b'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Attempting to write a chunk bigger than ChunkRecord::kMaxSize should end up |
| // in a no-op. |
| TEST_F(TraceBufferV2Test, Malicious_ChunkTooBig) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(4096, 'a') |
| .AddPacket(2048, 'b') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Malicious_DeclareMorePacketsBeyondBoundaries) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(64, 'a') |
| .IncrementNumPackets() |
| .IncrementNumPackets() |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(2), ChunkID(0)) |
| .IncrementNumPackets() |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(3), ChunkID(0)) |
| .AddPacket(32, 'b') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(64, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(32, 'b'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Malicious_ZeroVarintHeader) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| // Create a standalone chunk where the varint header is == 0. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(4, 'a') |
| .ClearBytes(0, 1) |
| .AddPacket(4, 'b') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(0)) |
| .AddPacket(4, 'c') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(4, 'c'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Forge a chunk where the first packet is valid but the second packet has a |
| // varint header that continues beyond the end of the chunk (and also beyond the |
| // end of the buffer). |
| TEST_F(TraceBufferV2Test, Malicious_OverflowingVarintHeader) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(4079, 'a') // 4079 := 4096 - sizeof(ChunkRecord) - 1 |
| .AddPacket({0x82}) // 0x8*: that the varint continues on the next byte. |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(4079, 'a'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Malicious_VarintHeaderTooBig) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| |
| // Add a valid chunk. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(32, 'a') |
| .CopyIntoTraceBuffer(); |
| |
| // Forge a packet which has a varint header that is just off by one. |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(0)) |
| .AddPacket({0x16, '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', |
| 'c', 'd', 'e', 'f'}) |
| .CopyIntoTraceBuffer(); |
| |
| // Forge a packet which has a varint header that tries to hit an overflow. |
| CreateChunk(ProducerID(3), WriterID(1), ChunkID(0)) |
| .AddPacket({0xff, 0xff, 0xff, 0x7f}) |
| .CopyIntoTraceBuffer(); |
| |
| // Forge a packet which has a jumbo varint header: 0xff, 0xff .. 0x7f. |
| std::vector<uint8_t> chunk; |
| chunk.insert(chunk.end(), 128 - sizeof(internal::TBChunk), 0xff); |
| chunk.back() = 0x7f; |
| trace_buffer()->CopyChunkUntrusted( |
| ProducerID(4), ClientIdentity(uid_t(0), pid_t(0)), WriterID(1), |
| ChunkID(1), 1 /* num packets */, 0 /* flags*/, true /* chunk_complete*/, |
| chunk.data(), chunk.size()); |
| |
| // Add a valid chunk. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(32, 'b') |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(32, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(32, 'b'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Similar to Malicious_VarintHeaderTooBig, but this time the full chunk |
| // contains an enormous varint number that tries to overflow. |
| TEST_F(TraceBufferV2Test, Malicious_JumboVarint) { |
| ResetBuffer(64 * 1024); |
| SuppressClientDchecksForTesting(); |
| |
| std::vector<uint8_t> chunk; |
| chunk.insert(chunk.end(), 64 * 1024 - sizeof(internal::TBChunk) * 2, 0xff); |
| chunk.back() = 0x7f; |
| for (int i = 0; i < 3; i++) { |
| trace_buffer()->CopyChunkUntrusted( |
| ProducerID(1), ClientIdentity(uid_t(0), pid_t(0)), WriterID(1), |
| ChunkID(1), 1 /* num packets */, 0 /* flags */, |
| true /* chunk_complete */, chunk.data(), chunk.size()); |
| } |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Like the Malicious_ZeroVarintHeader, but put the chunk in the middle of a |
| // sequence that would be otherwise valid. The zero-sized fragment should be |
| // skipped. |
| TEST_F(TraceBufferV2Test, Malicious_ZeroVarintHeaderInSequence) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(4, 'a', kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(4, 'b', kContFromPrevChunk | kContOnNextChunk) |
| .ClearBytes(0, 1) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(4, 'c', kContFromPrevChunk) |
| .AddPacket(4, 'd') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(3)) |
| .AddPacket(4, 'e') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(3)) |
| .AddPacket(5, 'f') |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(4, 'a'), |
| FakePacketFragment(4, 'c'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(4, 'd'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(4, 'e'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(5, 'f'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Similar to Malicious_ZeroVarintHeaderInSequence, but this time the zero-sized |
| // fragment is the last fragment for a chunk and is marked for continuation. The |
| // zero-sized fragment should be skipped. |
| TEST_F(TraceBufferV2Test, Malicious_ZeroVarintHeaderAtEndOfChunk) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(4, 'a') |
| .AddPacket(4, 'b', kContOnNextChunk) |
| .ClearBytes(4, 4) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(4, 'c', kContFromPrevChunk) |
| .AddPacket(4, 'd') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(4, 'e') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(3)) |
| .AddPacket(4, 'f') |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(4, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(4, 'c'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(4, 'd'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(4, 'e'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(4, 'f'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Malicious_PatchOutOfBounds) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(2048, 'a') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(16, 'b') |
| .CopyIntoTraceBuffer(); |
| size_t offsets[] = {13, 16, size_t(-4), |
| size_t(-8), size_t(-12), size_t(-16), |
| size_t(-20), size_t(-32), size_t(-1024)}; |
| for (size_t offset : offsets) { |
| ASSERT_FALSE(TryPatchChunkContents(ProducerID(1), WriterID(1), ChunkID(1), |
| {{offset, {{'0', 'd', 'a', 'y'}}}})); |
| } |
| } |
| |
| TEST_F(TraceBufferV2Test, Malicious_OverrideWithShorterChunkSize) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(2048, 'a') |
| .CopyIntoTraceBuffer(); |
| // The service should ignore this override of the chunk since the chunk size |
| // is different. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(1024, 'b') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(2048, 'a'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Malicious_OverrideWithShorterChunkSizeAfterRead) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(30, 'a') |
| .AddPacket(40, 'b') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(40, 'b'))); |
| |
| // The service should ignore this override of the chunk since the chunk size |
| // is different. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(10, 'a') |
| .AddPacket(10, 'b') |
| .AddPacket(10, 'c') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Test that the service didn't get stuck in some indeterminate state. |
| // Writing a valid chunk with a larger ID should make things work again. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(10, 'd') |
| .AddPacket(10, 'e') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'd'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'e'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Malicious_OverrideWithDifferentOffsetAfterRead) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(30, 'a') |
| .AddPacket(40, 'b') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(40, 'b'))); |
| |
| // The attacker in this case speculates on the fact that the read pointer is |
| // @ 70 which is >> the size of the new chunk we overwrite. |
| // The service will not discard this override since the chunk size is correct. |
| // However, it should detect that the packet headers at the current read |
| // offset are invalid and skip the read of this chunk. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(10, 'a') |
| .AddPacket(10, 'b') |
| .AddPacket(10, 'c') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Test that the service didn't get stuck in some indeterminate state. |
| // Writing a valid chunk with a larger ID should make things work again. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(10, 'd') |
| .AddPacket(10, 'e') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'd'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(10, 'e'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // ------------------- |
| // Re-writing same chunk id |
| // ------------------- |
| |
| TEST_F(TraceBufferV2Test, Override_ReCommitBeforeRead) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(100, 'a') |
| .AddPacket(100, 'b') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| EXPECT_EQ(0u, trace_buffer()->stats().chunks_rewritten()); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(100, 'a') |
| .AddPacket(100, 'b') |
| .AddPacket(100, 'c') |
| .AddPacket(100, 'd') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| EXPECT_EQ(1u, trace_buffer()->stats().chunks_rewritten()); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(100, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(100, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(100, 'c'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(100, 'd'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Override_ReCommitAfterPartialRead) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .AddPacket(40, 'c') |
| .AddPacket(50, 'd') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(40, 'c'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(50, 'd'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Override_ReCommitAfterFullRead) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .AddPacket(5, '_') // The last frag of an incomplete chunk is ignored. |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'b'))); |
| |
| // Overriding a complete packet here would trigger a DCHECK because the packet |
| // was already marked as complete. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .AddPacket(40, 'c') |
| .AddPacket(50, 'd') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(40, 'c'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(50, 'd'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // See also the Malicious_Override* tests above. |
| TEST_F(TraceBufferV2Test, Override_ReCommitInvalid) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(40, 'c') |
| .AddPacket(50, 'd') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(40, 'c'))); |
| |
| // This should not happen when the producer behaves correctly, since it |
| // shouldn't change the contents of chunk 0 after having allocated chunk 1. |
| // |
| // Since we've already started reading from chunk 1, TraceBufferV2 will |
| // recognize this and discard the override. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'e') |
| .AddPacket(60, 'f') |
| .AddPacket(70, 'g') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(50, 'd'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Override_ReCommitReordered) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| |
| // Recommit chunk 0 and add chunk 1, but do this out of order. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(50, 'd') |
| .AddPacket(60, 'e') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .AddPacket(40, 'c') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(40, 'c'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(50, 'd'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(60, 'e'))); |
| } |
| |
| TEST_F(TraceBufferV2Test, Override_ReCommitReorderedFragmenting) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| |
| // Recommit chunk 0 and add chunk 1, but do this out of order. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(50, 'd', kContFromPrevChunk) |
| .AddPacket(60, 'e') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .AddPacket(40, 'c', kContOnNextChunk) |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(40, 'c'), |
| FakePacketFragment(50, 'd'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(60, 'e'))); |
| } |
| |
| TEST_F(TraceBufferV2Test, Override_ReCommitSameBeforeRead) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| |
| // Commit again the same chunk. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| |
| // Then write some new content in a new chunk. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(40, 'c') |
| .AddPacket(50, 'd') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| |
| // The reader should keep reading from the new chunk. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(40, 'c'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(50, 'd'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Override_ReCommitSameAfterRead) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'b'))); |
| |
| // This re-commit should be ignored. We just re-committed an identical chunk. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| |
| // Then write some new content in a new chunk. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(40, 'c') |
| .AddPacket(50, 'd') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| |
| // The reader should keep reading from the new chunk. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(40, 'c'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(50, 'd'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Override_ReCommitIncompleteAfterReadOutOfOrder) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| // The last packet in an incomplete chunk should be ignored as the producer |
| // may not have completed writing it. |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Then write some new content in a new chunk. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(40, 'c') |
| .AddPacket(50, 'd') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| // The read still shouldn't be advancing past the incomplete chunk. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Recommit the original chunk with no changes but mark as complete. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/true); |
| |
| // Reading should resume from the now completed chunk. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(40, 'c'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(50, 'd'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Override_ReCommitIncompleteFragmenting) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b', kContOnNextChunk) |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| // The last packet in an incomplete chunk should be ignored as the producer |
| // may not have completed writing it. |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Then write some new content in a new chunk. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(40, 'c', kContFromPrevChunk) |
| .AddPacket(50, 'd') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| // The read still shouldn't be advancing past the incomplete chunk. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Recommit the original chunk with no changes but mark as complete. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b', kContOnNextChunk) |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/true); |
| |
| // Reading should resume from the now completed chunk. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'b'), |
| FakePacketFragment(40, 'c'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(50, 'd'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Override_EndOfBuffer) { |
| ResetBuffer(3072); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .PadTo(2048) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| // The last packet in an incomplete chunk should be ignored as the producer |
| // may not have completed writing it. |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Recommit the original chunk with no changes but mark as complete. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .PadTo(2048) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/true); |
| |
| // Reading should resume from the now completed chunk. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'b'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, DiscardPolicy) { |
| ResetBuffer(4096, TraceBufferV2::kDiscard); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(32 - 16, 'a') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(4000 - 16, 'b') |
| .CopyIntoTraceBuffer(); |
| // Leave 32 bytes free at the end of the buffer. |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(32 - 16, 'a'))); |
| |
| // This should still fit |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(20 - 16, 'c') |
| .CopyIntoTraceBuffer(); |
| |
| // Neither of these should fit. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(3)) |
| .AddPacket(48 - 16, 'x') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(4)) |
| .AddPacket(48 - 16, 'x') |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(4000 - 16, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20 - 16, 'c'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // More writes should still be discarded. |
| for (int i = 0; i < 3; i++) { |
| CreateChunk(ProducerID(1), WriterID(i + 10), ChunkID(0)) |
| .AddPacket(64 - 16, 'X') |
| .CopyIntoTraceBuffer(); |
| } |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, NoDataLossIfReaderCatchesUp) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| |
| for (WriterID i = 0; i < 3; i++) { |
| CreateChunk(ProducerID(1), WriterID(i), ChunkID(0)) |
| .AddPacket(2000, 'a') |
| .CopyIntoTraceBuffer(); |
| |
| CreateChunk(ProducerID(1), WriterID(i), ChunkID(1)) |
| .AddPacket(1000, 'b') |
| .CopyIntoTraceBuffer(); |
| |
| uint32_t previous_packet_dropped = 0; |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(2000, 'a'))); |
| ASSERT_FALSE(previous_packet_dropped); |
| |
| // This will wrap and get written @ wr_ = 0. |
| CreateChunk(ProducerID(1), WriterID(i), ChunkID(2)) |
| .AddPacket(2000, 'c') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(1000, 'b'))); |
| ASSERT_FALSE(previous_packet_dropped); |
| |
| CreateChunk(ProducerID(1), WriterID(i), ChunkID(3)) |
| .AddPacket(2000, 'd') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(2000, 'c'))); |
| ASSERT_FALSE(previous_packet_dropped); |
| |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(2000, 'd'))); |
| ASSERT_FALSE(previous_packet_dropped); |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), IsEmpty()); |
| } |
| } |
| |
| TEST_F(TraceBufferV2Test, PacketDropOnOverwrite) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(10, 'a') |
| .CopyIntoTraceBuffer(); |
| |
| uint32_t previous_packet_dropped = 0; |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(10, 'a'))); |
| ASSERT_FALSE(previous_packet_dropped); |
| |
| // Write two large chunks that don't fit into the buffer at the same time. We |
| // will drop the former one before we can read it. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(2000, 'b') |
| .CopyIntoTraceBuffer(); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(3)) |
| .AddPacket(3000, 'c') |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(3000, 'c'))); |
| ASSERT_TRUE(previous_packet_dropped); |
| } |
| |
| TEST_F(TraceBufferV2Test, Clone_NoFragments) { |
| ResetBuffer(4096); |
| const char kNumWriters = 3; |
| for (char i = 'A'; i < 'A' + kNumWriters; i++) { |
| ASSERT_EQ(32u, CreateChunk(ProducerID(0), WriterID(i), ChunkID(0)) |
| .AddPacket(32 - 16, i) |
| .CopyIntoTraceBuffer()); |
| } |
| |
| // Now create a snapshot and make sure we always read all the packets. |
| std::unique_ptr<TraceBuffer> snap = trace_buffer()->CloneReadOnly(); |
| trace_buffer_.reset(); |
| |
| ASSERT_EQ(snap->used_size(), 32u * kNumWriters); |
| snap->BeginRead(); |
| for (char i = 'A'; i < 'A' + kNumWriters; i++) { |
| auto frags = ReadPacket(snap); |
| ASSERT_THAT(frags, ElementsAre(FakePacketFragment(32 - 16, i))); |
| } |
| ASSERT_THAT(ReadPacket(snap), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Clone_FragmentsOutOfOrder) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(10, 'a') |
| .AddPacket(10, '_') |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(30, 'd') |
| .CopyIntoTraceBuffer(); |
| |
| { |
| // Create a snapshot before the middle chunk is copied. Only 'a' should |
| // be readable at this point. |
| std::unique_ptr<TraceBuffer> snap = trace_buffer()->CloneReadOnly(); |
| snap->BeginRead(); |
| ASSERT_THAT(ReadPacket(snap), ElementsAre(FakePacketFragment(10, 'a'))); |
| ASSERT_THAT(ReadPacket(snap), IsEmpty()); |
| } |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(20, 'c') |
| .CopyIntoTraceBuffer(); |
| |
| // Recommit (out of order) chunk 0, marking it as complete this time. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(10, 'a') |
| .AddPacket(10, 'b') |
| .CopyIntoTraceBuffer(); |
| |
| // Now all three packes should be readable. |
| std::unique_ptr<TraceBuffer> snap = trace_buffer()->CloneReadOnly(); |
| snap->BeginRead(); |
| ASSERT_THAT(ReadPacket(snap), ElementsAre(FakePacketFragment(10, 'a'))); |
| ASSERT_THAT(ReadPacket(snap), ElementsAre(FakePacketFragment(10, 'b'))); |
| ASSERT_THAT(ReadPacket(snap), ElementsAre(FakePacketFragment(20, 'c'))); |
| ASSERT_THAT(ReadPacket(snap), ElementsAre(FakePacketFragment(30, 'd'))); |
| ASSERT_THAT(ReadPacket(snap), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Clone_WithPatches) { |
| ResetBuffer(4096); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(100, 'a') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(0)) |
| .AddPacket(9, 'b') |
| .ClearBytes(5, 4) // 5 := 4th payload byte. Byte 0 is the varint header |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(3), WriterID(1), ChunkID(0)) |
| .AddPacket(100, 'c') |
| .CopyIntoTraceBuffer(); |
| ASSERT_TRUE(TryPatchChunkContents(ProducerID(2), WriterID(1), ChunkID(0), |
| {{5, {{'Y', 'M', 'C', 'A'}}}})); |
| |
| std::unique_ptr<TraceBuffer> snap = trace_buffer()->CloneReadOnly(); |
| snap->BeginRead(); |
| ASSERT_THAT(ReadPacket(snap), ElementsAre(FakePacketFragment(100, 'a'))); |
| ASSERT_THAT(ReadPacket(snap), ElementsAre(FakePacketFragment("b00-YMCA", 8))); |
| ASSERT_THAT(ReadPacket(snap), ElementsAre(FakePacketFragment(100, 'c'))); |
| ASSERT_THAT(ReadPacket(snap), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Clone_Wrapping) { |
| ResetBuffer(4096); |
| const size_t kFrgSize = 1024 - 16; // For perfect wrapping every 4 fragments |
| for (WriterID i = 0; i < 6; i++) { |
| CreateChunk(ProducerID(1), WriterID(i), ChunkID(0)) |
| .AddPacket(kFrgSize, static_cast<char>('a' + i)) |
| .CopyIntoTraceBuffer(); |
| } |
| std::unique_ptr<TraceBuffer> snap = trace_buffer()->CloneReadOnly(); |
| ASSERT_EQ(snap->used_size(), snap->size()); |
| snap->BeginRead(); |
| ASSERT_THAT(ReadPacket(snap), ElementsAre(FakePacketFragment(kFrgSize, 'c'))); |
| ASSERT_THAT(ReadPacket(snap), ElementsAre(FakePacketFragment(kFrgSize, 'd'))); |
| ASSERT_THAT(ReadPacket(snap), ElementsAre(FakePacketFragment(kFrgSize, 'e'))); |
| ASSERT_THAT(ReadPacket(snap), ElementsAre(FakePacketFragment(kFrgSize, 'f'))); |
| ASSERT_THAT(ReadPacket(snap), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Clone_WrappingWithPadding) { |
| ResetBuffer(4096); |
| // First create one 2KB chunk, so the contents are [aaaaaaaa00000000]. |
| CreateChunk(ProducerID(1), WriterID(0), ChunkID(0)) |
| .AddPacket(2048, static_cast<char>('a')) |
| .CopyIntoTraceBuffer(); |
| |
| // Then write a 3KB chunk that fits in the buffer, but requires zero padding. |
| // and restarting from the beginning, so the contents are [bbbbbbbbbbbb0000]. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(3192, static_cast<char>('b')) |
| .CopyIntoTraceBuffer(); |
| |
| std::unique_ptr<TraceBuffer> snap = trace_buffer()->CloneReadOnly(); |
| ASSERT_EQ(snap->used_size(), internal::TBChunk::OuterSize(3192u)); |
| snap->BeginRead(); |
| ASSERT_THAT(ReadPacket(snap), ElementsAre(FakePacketFragment(3192, 'b'))); |
| ASSERT_THAT(ReadPacket(snap), IsEmpty()); |
| } |
| |
| TEST_F(TraceBufferV2Test, Clone_CommitOnlyUsedSize) { |
| const size_t kPages = 32; |
| const size_t page_size = base::GetSysPageSize(); |
| ResetBuffer(page_size * kPages); |
| CreateChunk(ProducerID(1), WriterID(0), ChunkID(0)) |
| .AddPacket(1024, static_cast<char>('a')) |
| .CopyIntoTraceBuffer(); |
| |
| using base::vm_test_utils::IsMapped; |
| auto is_only_first_page_mapped = [&](const TraceBuffer& buf) { |
| bool first_mapped = IsMapped(GetBufData(buf), page_size); |
| bool rest_mapped = IsMapped(GetBufData(buf) + page_size, kPages - 1); |
| return first_mapped && !rest_mapped; |
| }; |
| |
| // If the test doesn't work as expected until here, there is no point checking |
| // that the same assumptions hold true on the cloned buffer. Various platforms |
| // can legitimately pre-fetch memory even if we don't page fault (also asan). |
| if (!is_only_first_page_mapped(*trace_buffer())) |
| GTEST_SKIP() << "VM commit detection not supported"; |
| |
| std::unique_ptr<TraceBuffer> snap = trace_buffer()->CloneReadOnly(); |
| ASSERT_EQ(snap->used_size(), trace_buffer()->used_size()); |
| ASSERT_TRUE(is_only_first_page_mapped(*snap)); |
| } |
| |
| TEST_F(TraceBufferV2Test, ChunkGaps_WithinSameReadCycle) { |
| ResetBuffer(4096); |
| |
| ASSERT_EQ(32u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(32 - 16, 'a') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(32u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(3)) |
| .AddPacket(32 - 16, 'c') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(32u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(4)) |
| .AddPacket(32 - 16, 'd') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(32u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(6)) |
| .AddPacket(32 - 16, 'f') |
| .CopyIntoTraceBuffer()); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(32 - 16, 'a'))); |
| |
| uint32_t previous_packet_dropped = 0; |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(32 - 16, 'c'))); |
| EXPECT_TRUE(previous_packet_dropped); |
| |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(32 - 16, 'd'))); |
| EXPECT_FALSE(previous_packet_dropped); |
| |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(32 - 16, 'f'))); |
| EXPECT_TRUE(previous_packet_dropped); |
| } |
| |
| TEST_F(TraceBufferV2Test, ChunkGaps_AcrossReadCycles) { |
| ResetBuffer(4096); |
| |
| // Write and consume a chunk. |
| ASSERT_EQ(32u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(32 - 16, 'a') |
| .CopyIntoTraceBuffer()); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(32 - 16, 'a'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Now write an consume another chunk keeping the sequence in order, and |
| // ensure no data loss is reported. |
| ASSERT_EQ(32u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(32 - 16, 'b') |
| .CopyIntoTraceBuffer()); |
| uint32_t previous_packet_dropped = 0; |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(32 - 16, 'b'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| EXPECT_FALSE(previous_packet_dropped); |
| |
| // Now write an consume another chunk, but create a gap in the chunk id. |
| ASSERT_EQ(32u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(4)) |
| .AddPacket(32 - 16, 'd') |
| .CopyIntoTraceBuffer()); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(32 - 16, 'd'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| EXPECT_TRUE(previous_packet_dropped); |
| |
| // Now write an consume another chunk, but create a gap in the chunk id. |
| ASSERT_EQ(32u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(5)) |
| .AddPacket(32 - 16, 'e') |
| .CopyIntoTraceBuffer()); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(32 - 16, 'e'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| EXPECT_FALSE(previous_packet_dropped); |
| } |
| |
| // Regression test for a now-fixed long-standing issue about signalling a |
| // false positive data loss when using periodic reads (e.g. write_into_file). |
| // See b/268257546, https://github.com/google/perfetto/issues/114. |
| TEST_F(TraceBufferV2Test, ChunkGaps_EvenIfSequenceDisappears) { |
| ResetBuffer(4096); |
| |
| // Write and consume a chunk. |
| ASSERT_EQ(32u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(32 - 16, 'a') |
| .CopyIntoTraceBuffer()); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(32 - 16, 'a'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Now write some large chunks from another sequence that will completely |
| // obliterate the buffer. |
| ASSERT_EQ(4096u, CreateChunk(ProducerID(42), WriterID(1), ChunkID(1)) |
| .AddPacket(4096 - 16, '_') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(4096u, CreateChunk(ProducerID(42), WriterID(1), ChunkID(2)) |
| .AddPacket(4096 - 16, '_') |
| .CopyIntoTraceBuffer()); |
| |
| // This one is contiguous and shoudl't report any data loss. |
| ASSERT_EQ(32u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(32 - 16, 'b') |
| .CopyIntoTraceBuffer()); |
| uint32_t previous_packet_dropped = 0; |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(32 - 16, 'b'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| EXPECT_FALSE(previous_packet_dropped); |
| |
| // Clobber the buffer again. |
| ASSERT_EQ(4096u, CreateChunk(ProducerID(42), WriterID(1), ChunkID(3)) |
| .AddPacket(4096 - 16, '_') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(4096u, CreateChunk(ProducerID(42), WriterID(1), ChunkID(4)) |
| .AddPacket(4096 - 16, '_') |
| .CopyIntoTraceBuffer()); |
| |
| // This one has a discontinutiy (2 -> 4) and should report a data loss. |
| ASSERT_EQ(32u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(4)) |
| .AddPacket(32 - 16, 'd') |
| .CopyIntoTraceBuffer()); |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(32 - 16, 'd'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| EXPECT_TRUE(previous_packet_dropped); |
| } |
| |
| TEST_F(TraceBufferV2Test, WrapAroundWithIncompleteChunk) { |
| ResetBuffer(4096); |
| |
| // Commit C1, C2, C3 chunks of 1024 bytes each (1008 bytes payload + 16 bytes |
| // header) |
| ASSERT_EQ(1024u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(1008, '1') |
| .CopyIntoTraceBuffer()); |
| |
| // Mark C2 as incomplete - this chunk should be overwritten and not preserved |
| ASSERT_EQ(1024u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(1008, '2') |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false)); |
| |
| ASSERT_EQ(1024u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(3)) |
| .AddPacket(1008, '3') |
| .CopyIntoTraceBuffer()); |
| |
| // Buffer now contains: [C1: 1024][C2: 1024 incomplete][C3: 1024][1024 free] |
| |
| // Write C4, C5, C6 to cause wrap around - these will overwrite C1, C2, and |
| // start to overwrite C3 But since C2 is incomplete, C3 should be preserved |
| ASSERT_EQ(1024u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(4)) |
| .AddPacket(1008, '4') |
| .CopyIntoTraceBuffer()); |
| |
| ASSERT_EQ(1024u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(5)) |
| .AddPacket(1008, '5') |
| .CopyIntoTraceBuffer()); |
| |
| ASSERT_EQ(1024u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(6)) |
| .AddPacket(1008, '6') |
| .CopyIntoTraceBuffer()); |
| |
| // Buffer should now contain: [C4: 1024][C5: 1024][C6: 1024][C3: 1024] |
| // We should be able to read C3, C4, C5, C6 in that order |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(1008, '3'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(1008, '4'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(1008, '5'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(1008, '6'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Test ChunkID wraparound with complex fragmentation |
| TEST_F(TraceBufferV2Test, Fragments_ChunkIdMaxWraparoundFragmentation) { |
| ResetBuffer(4096); |
| std::vector<FakePacketFragment> expected; |
| |
| // Create a fragmented packet spanning ChunkID wraparound from UINT32_MAX to 2 |
| ChunkID start_id = static_cast<ChunkID>(-2); |
| for (uint32_t i = 0; i < 5; ++i) { |
| ChunkID chunk_id = start_id + i; |
| uint8_t flags = 0; |
| char data = static_cast<char>('a' + i); |
| |
| if (i == 0) |
| flags = kContOnNextChunk; |
| else if (i == 4) |
| flags = kContFromPrevChunk; |
| else |
| flags = kContFromPrevChunk | kContOnNextChunk; |
| |
| CreateChunk(ProducerID(1), WriterID(1), chunk_id) |
| .AddPacket(10, data, flags) |
| .CopyIntoTraceBuffer(); |
| expected.emplace_back(10, data); |
| } |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ContainerEq(expected)); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Test buffer boundary alignment with fragmentation |
| TEST_F(TraceBufferV2Test, Alignment_ExactBufferBoundaryFragmentation) { |
| ResetBuffer(4096); |
| |
| // Create a packet that fragments exactly at buffer boundaries |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(2032 - 16, 'a', kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(2048 - 16, 'b', kContFromPrevChunk) |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(2032 - 16, 'a'), |
| FakePacketFragment(2048 - 16, 'b'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // In discard mode, once the buffer fills exactly (chunks land at the end), |
| // subsequent writes must be dropped rather than overwriting earlier data. |
| TEST_F(TraceBufferV2Test, DiscardPolicy_FillsExactlyThenDiscards) { |
| ResetBuffer(4096, TraceBufferV2::kDiscard); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(2030, 'a') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(2030, 'b') |
| .CopyIntoTraceBuffer(); |
| |
| // Buffer is now full. This write must be discarded. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(2030, 'c') |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(2030, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(2030, 'b'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Verify write_wrap_count increments every time a write needs to wrap, and |
| // that filling the buffer exactly defers the increment until the next write. |
| TEST_F(TraceBufferV2Test, WriteWrapCount) { |
| ResetBuffer(4096); |
| EXPECT_EQ(0u, trace_buffer()->stats().write_wrap_count()); |
| |
| // Fill the buffer exactly. Wrap is not processed yet. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(2030, 'a') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(2030, 'b') |
| .CopyIntoTraceBuffer(); |
| EXPECT_EQ(0u, trace_buffer()->stats().write_wrap_count()); |
| |
| // Next write: buffer has no tail space, wrap is processed. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(3054, 'c') |
| .CopyIntoTraceBuffer(); |
| EXPECT_EQ(1u, trace_buffer()->stats().write_wrap_count()); |
| |
| // Next write: tail space insufficient for the chunk, wrap again. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(3)) |
| .AddPacket(2030, 'd') |
| .CopyIntoTraceBuffer(); |
| EXPECT_EQ(2u, trace_buffer()->stats().write_wrap_count()); |
| } |
| |
| // Test out-of-order patch application with fragmentation |
| TEST_F(TraceBufferV2Test, Patching_OutOfOrderPatchesWithFragmentation) { |
| ResetBuffer(4096); |
| |
| // Create fragmented packet needing patches on multiple chunks |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(100, 'a', kContOnNextChunk | kChunkNeedsPatching) |
| .ClearBytes(50, 4) |
| .CopyIntoTraceBuffer(); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(100, 'c', kContFromPrevChunk) |
| .CopyIntoTraceBuffer(); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(100, 'b', |
| kContFromPrevChunk | kContOnNextChunk | kChunkNeedsPatching) |
| .ClearBytes(50, 4) |
| .CopyIntoTraceBuffer(); |
| |
| // Apply patches out of order |
| ASSERT_TRUE(TryPatchChunkContents(ProducerID(1), WriterID(1), ChunkID(1), |
| {{50, {{'B', 'B', 'B', 'B'}}}})); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); // Still blocked by chunk 0 |
| |
| ASSERT_TRUE(TryPatchChunkContents(ProducerID(1), WriterID(1), ChunkID(0), |
| {{50, {{'A', 'A', 'A', 'A'}}}})); |
| |
| trace_buffer()->BeginRead(); |
| // The patches should have been applied, changing the actual payload content |
| auto packet_frags = ReadPacket(); |
| ASSERT_EQ(packet_frags.size(), 3u); |
| // Verify patches were actually applied by checking the modified payload |
| // content The patches AAAA and BBBB should be visible in the payload |
| EXPECT_NE(packet_frags[0].payload().find("AAAA"), std::string::npos); |
| EXPECT_NE(packet_frags[1].payload().find("BBBB"), std::string::npos); |
| } |
| |
| // Test recommit from incomplete to complete with fragmentation |
| TEST_F(TraceBufferV2Test, Recommit_IncompleteToCompleteWithFragments) { |
| ResetBuffer(4096); |
| |
| // Create incomplete chunk |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(50, 'a') |
| .AddPacket(50, 'b') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(50, 'a'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); // Blocked by incomplete chunk |
| |
| // Recommit as complete with 'c' fragment that continues to next chunk |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(50, 'a') |
| .AddPacket(50, 'b') |
| .AddPacket(30, 'c') |
| .SetFlags(kContOnNextChunk) |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/true); |
| |
| // Add continuation chunk with fragmented packet spanning across chunks |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(40, 'd') |
| .SetFlags(kContFromPrevChunk | kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(20, 'e') |
| .SetFlags(kContFromPrevChunk) |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(50, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'c'), |
| FakePacketFragment(40, 'd'), |
| FakePacketFragment(20, 'e'))); |
| } |
| |
| // Test DISCARD mode with fragmented packet at buffer limit |
| TEST_F(TraceBufferV2Test, DiscardMode_FragmentedPacketAtBoundary) { |
| ResetBuffer(4096, TraceBuffer::kDiscard); |
| |
| // Fill most of buffer - leave just enough space for part of a fragmented |
| // packet |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(2000, 'a') |
| .CopyIntoTraceBuffer(); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(1500, 'b') |
| .CopyIntoTraceBuffer(); |
| |
| // Add chunk with multiple fragments, last one continuing to next chunk |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(200, 'c') |
| .AddPacket(150, 'd') |
| .AddPacket(100, 'e') // This fragment continues to next chunk |
| .SetFlags(kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| |
| // This continuation should be discarded as it would overflow the buffer |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(3)) |
| .AddPacket(500, 'f') |
| .SetFlags(kContFromPrevChunk) |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(2000, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(1500, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(200, 'c'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(150, 'd'))); |
| // The fragmented packet 'e'+'f' should be incomplete due to discard |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Test maximum fragment count in a single packet |
| TEST_F(TraceBufferV2Test, Fragments_LargeFragment) { |
| ResetBuffer(8192); |
| std::vector<FakePacketFragment> expected; |
| |
| // Create a packet fragmented across 10 chunks |
| for (uint32_t i = 0; i < 10; ++i) { |
| uint8_t flags = 0; |
| char data = static_cast<char>('a' + i); |
| |
| if (i == 0) |
| flags = kContOnNextChunk; |
| else if (i == 9) |
| flags = kContFromPrevChunk; |
| else |
| flags = kContFromPrevChunk | kContOnNextChunk; |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(i)) |
| .AddPacket(50, data, flags) |
| .CopyIntoTraceBuffer(); |
| expected.emplace_back(50, data); |
| } |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ContainerEq(expected)); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Test empty chunks in long fragmentation chain |
| TEST_F(TraceBufferV2Test, Fragments_EmptyChunksInLongChain) { |
| ResetBuffer(4096); |
| std::vector<FakePacketFragment> expected; |
| |
| // Create fragmented packet with empty chunks in between |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a', kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| expected.emplace_back(20, 'a'); |
| |
| // Empty chunk in the middle |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)).CopyIntoTraceBuffer(); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(20, 'b', kContFromPrevChunk | kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| expected.emplace_back(20, 'b'); |
| |
| // Another empty chunk |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(3)).CopyIntoTraceBuffer(); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(4)) |
| .AddPacket(20, 'c', kContFromPrevChunk) |
| .CopyIntoTraceBuffer(); |
| expected.emplace_back(20, 'c'); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ContainerEq(expected)); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Test sequence gap detection across ChunkID wraparound |
| TEST_F(TraceBufferV2Test, SequenceGaps_DetectionWithChunkIdWrap) { |
| ResetBuffer(4096); |
| |
| // Normal sequence |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(kMaxChunkID - 1)) |
| .AddPacket(32, 'a') |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(32, 'a'))); |
| |
| // Continuation across wraparound - no gap |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(kMaxChunkID)) |
| .AddPacket(32, 'b') |
| .CopyIntoTraceBuffer(); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(32, 'c') |
| .CopyIntoTraceBuffer(); |
| |
| uint32_t previous_packet_dropped = 0; |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(32, 'b'))); |
| EXPECT_FALSE(previous_packet_dropped); |
| |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(32, 'c'))); |
| EXPECT_FALSE(previous_packet_dropped); |
| |
| // Now create a gap across wraparound |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(3)) // Gap: missing 1,2 |
| .AddPacket(32, 'd') |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(32, 'd'))); |
| EXPECT_TRUE(previous_packet_dropped); // Gap should be detected |
| } |
| |
| // We try to write a 36 byte chunk with a 32 byte chunk, which leaves just a |
| // 4 byte gap. That gap is not enough for a TBChunk header, without deleting |
| // also c1. |
| // This test today passes because we force the TBChunk alignment at 16 bytes |
| // rather than 4 (see TODO in TBChunk::OuterSize()). If we put this back to 4 |
| // this test will break, until we figure out how to deal with this corner case. |
| // Before: [c0: 36 ][c1: 4060 ] |
| // After: [c2: 32 ] |
| // Note that the same could happen at the end of the buffer: imagine 36 byte |
| // chunk that starts precisely @ 4096 - 36, and then get overwritten by one of |
| // 32 bytes. |
| TEST_F(TraceBufferV2Test, Overwrite_SizeDiffLessThanChunkHeader) { |
| ResetBuffer(4096); |
| |
| size_t c0_size = 36; |
| ASSERT_EQ(c0_size, CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(c0_size - 16, 'a') |
| .CopyIntoTraceBuffer()); |
| size_t pad_size = 4096 - internal::TBChunk::OuterSize(c0_size - 16); |
| ASSERT_EQ(pad_size, CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(pad_size - 16, 'b') |
| .CopyIntoTraceBuffer()); |
| ASSERT_EQ(0u, size_to_end()); |
| |
| // The third commit uses a fresh chunk_id so the wrap path overwrites c0 |
| // ('a') with c2 ('c'). This exercises the case where the leftover space |
| // after the new chunk is exactly sizeof(TBChunk) (= 16B) — a header-only |
| // padding chunk. |
| ASSERT_EQ(32u, CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(32 - 16, 'c') |
| .CopyIntoTraceBuffer()); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), |
| ElementsAre(FakePacketFragment(pad_size - 16, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(32 - 16, 'c'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // ------------------------------------------- |
| // Re-scrape after ring-buffer eviction tests |
| // ------------------------------------------- |
| |
| // V2 tracks last_chunk_consumed metadata per writer sequence to handle |
| // re-scraping after eviction. Complete chunks that have been consumed are |
| // rejected on re-introduction. Incomplete (scraped) chunks that were evicted |
| // can be re-admitted if the new commit carries strictly more payload, allowing |
| // recovery of fragments dropped during scraping. |
| |
| // Scrape → read → evict → second scrape with more data. The new scrape has |
| // strictly more payload and must be re-admitted, recovering the new fragments |
| // without duplicating already-consumed data. |
| TEST_F(TraceBufferV2Test, RescrapeAfterEviction_FullyRead) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| |
| // Scrape chunk 0 (incomplete): [Whole 'a'] [kFragBegin 'b']. |
| // Scraping drops 'b'. Only 'a' is visible. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(10, 'b', kContOnNextChunk) |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Fill to force wraparound. |
| for (ChunkID c = 1; c <= 4; c++) { |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(c)) |
| .AddPacket(1024 - 16, static_cast<char>('x')) |
| .CopyIntoTraceBuffer(); |
| } |
| |
| // Second scrape: producer wrote more data. Now chunk 0 has [a, b, c]. |
| // Scraping drops 'c', stores [a, b]. More payload than first scrape. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .AddPacket(10, 'c', kContOnNextChunk) |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| |
| // Drain and verify: 'b' recovered, 'a' not duplicated. The recovered 'b' |
| // must not be flagged with previous_packet_dropped — the re-admit landed |
| // on the same chunk_id as last_consumed, and the gap check in |
| // ChunkSeqReader must treat that as gapless rather than firing spuriously. |
| trace_buffer()->BeginRead(); |
| std::vector<std::vector<FakePacketFragment>> packets; |
| uint32_t b_dropped = 0; |
| for (;;) { |
| uint32_t dropped = 0; |
| auto p = ReadPacket(/*sequence_properties=*/nullptr, &dropped); |
| if (p.empty()) |
| break; |
| if (p.size() == 1 && p[0] == FakePacketFragment(30, 'b')) |
| b_dropped = dropped; |
| packets.push_back(std::move(p)); |
| } |
| |
| bool found_a = false, found_b = false, found_c = false; |
| for (const auto& pkt : packets) { |
| if (pkt.size() == 1 && pkt[0] == FakePacketFragment(20, 'a')) |
| found_a = true; |
| if (pkt.size() == 1 && pkt[0] == FakePacketFragment(30, 'b')) |
| found_b = true; |
| if (pkt.size() == 1 && pkt[0] == FakePacketFragment(10, 'c')) |
| found_c = true; |
| } |
| EXPECT_TRUE(found_b) << "Packet 'b' not found after rescrape re-admission"; |
| EXPECT_FALSE(b_dropped) << "Re-admitted 'b' falsely flagged as dropped — " |
| "ChunkSeqReader gap check fired on re-admit."; |
| EXPECT_FALSE(found_a) << "Packet 'a' duplicated after rescrape re-admission"; |
| EXPECT_FALSE(found_c) |
| << "Packet 'c' should still be dropped (chunk incomplete)"; |
| } |
| |
| // Scrape → read → evict → producer completes chunk. The complete commit has |
| // strictly more payload and must be re-admitted, recovering the previously- |
| // dropped fragment and allowing cross-chunk reassembly. |
| TEST_F(TraceBufferV2Test, RescrapeAfterEviction_CompleteCommit) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| |
| // Scrape chunk 0 (incomplete): [Whole 'a'] [Whole 'b'] [kFragBegin 'c']. |
| // Scraping drops 'c', clears kContOnNextChunk, sets kChunkIncomplete. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .AddPacket(10, 'c', kContOnNextChunk) |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'b'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Fill to force wraparound. |
| for (ChunkID c = 1; c <= 4; c++) { |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(c)) |
| .AddPacket(1024 - 16, static_cast<char>('x')) |
| .CopyIntoTraceBuffer(); |
| } |
| |
| // Producer commits chunk 0 as complete with full payload [a, b, c]. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .AddPacket(10, 'c', kContOnNextChunk) |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/true); |
| |
| // Chunk 1: continuation of 'c' plus a whole packet 'e'. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(10, 'd', kContFromPrevChunk) |
| .AddPacket(20, 'e') |
| .CopyIntoTraceBuffer(); |
| |
| // Drain and verify: [c,d] and 'e' recovered, 'a' and 'b' not duplicated. |
| trace_buffer()->BeginRead(); |
| std::vector<std::vector<FakePacketFragment>> packets; |
| for (;;) { |
| auto p = ReadPacket(); |
| if (p.empty()) |
| break; |
| packets.push_back(std::move(p)); |
| } |
| |
| bool found_cd = false, found_e = false, found_a = false, found_b = false; |
| for (const auto& pkt : packets) { |
| if (pkt.size() == 2 && pkt[0] == FakePacketFragment(10, 'c') && |
| pkt[1] == FakePacketFragment(10, 'd')) |
| found_cd = true; |
| if (pkt.size() == 1 && pkt[0] == FakePacketFragment(20, 'e')) |
| found_e = true; |
| if (pkt.size() == 1 && pkt[0] == FakePacketFragment(20, 'a')) |
| found_a = true; |
| if (pkt.size() == 1 && pkt[0] == FakePacketFragment(30, 'b')) |
| found_b = true; |
| } |
| EXPECT_TRUE(found_cd) << "Reassembled packet [c,d] not found"; |
| EXPECT_TRUE(found_e) << "Packet 'e' not found"; |
| EXPECT_FALSE(found_a) << "Packet 'a' duplicated after complete re-admission"; |
| EXPECT_FALSE(found_b) << "Packet 'b' duplicated after complete re-admission"; |
| } |
| |
| // After a chunk is partially read and the buffer wraps, re-introducing the |
| // same chunk should not produce duplicate packets for already-read fragments. |
| TEST_F(TraceBufferV2Test, RescrapeAfterEviction_PartiallyRead) { |
| ResetBuffer(4096); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .AddPacket(40, 'c') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(40, 'c'))); |
| |
| // Force wraparound. |
| for (ChunkID c = 1; c <= 4; c++) { |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(c)) |
| .AddPacket(1024 - 16, static_cast<char>('x')) |
| .CopyIntoTraceBuffer(); |
| } |
| |
| // Re-introduce chunk {1,1,0} with extra data. |
| SuppressClientDchecksForTesting(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .AddPacket(40, 'c') |
| .AddPacket(50, 'd') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| |
| // Drain and collect. |
| trace_buffer()->BeginRead(); |
| std::vector<std::vector<FakePacketFragment>> packets; |
| for (;;) { |
| auto p = ReadPacket(); |
| if (p.empty()) |
| break; |
| packets.push_back(std::move(p)); |
| } |
| |
| // 'a' must not be re-read. V2 rejects the entire chunk since its ID has |
| // already been consumed. |
| bool found_a = false; |
| for (const auto& pkt : packets) { |
| if (pkt.size() == 1 && pkt[0] == FakePacketFragment(20, 'a')) |
| found_a = true; |
| } |
| EXPECT_FALSE(found_a) << "Packet 'a' was re-read after eviction+rescrape"; |
| } |
| |
| // Regression test for the scrape-recommit-read interaction. When a chunk is |
| // scraped (chunk_complete=false), it gets kChunkIncomplete. If the same chunk |
| // is scraped again with the same payload, the recommit path must NOT clear |
| // kChunkIncomplete. Otherwise the reader would consume it prematurely, and when |
| // the real IPC commit arrives it's discarded as "late", orphaning the kFragEnd |
| // in the next chunk. |
| TEST_F(TraceBufferV2Test, ScrapeRecommitPreservesIncomplete) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| |
| // Scrape chunk 0: [Whole 'a'] [Whole 'b'] [kFragBegin 'c' kContOnNextChunk]. |
| // Scraping drops the last fragment ('c'), clears kContOnNextChunk, sets |
| // kChunkIncomplete. Only fragments 'a' and 'b' are visible. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .AddPacket(10, 'c', kContOnNextChunk) |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| |
| // Read cycle 1: gets 'a' and 'b'. The chunk's payload is fully consumed but |
| // it is NOT erased because kChunkIncomplete prevents it. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'b'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Second scrape of the same chunk (same payload). This triggers the recommit |
| // path. The fix ensures kChunkIncomplete is NOT cleared here. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .AddPacket(10, 'c', kContOnNextChunk) |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| |
| // Read cycle 2: chunk still has kChunkIncomplete, so nothing new to read. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // The chunk must NOT have been consumed (no chunks_discarded bump yet). |
| EXPECT_EQ(0u, trace_buffer()->stats().chunks_discarded()); |
| |
| // IPC recommit: producer finished writing, commits as complete. This clears |
| // kChunkIncomplete and restores the full payload including the previously |
| // dropped fragment 'c'. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .AddPacket(10, 'c', kContOnNextChunk) |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/true); |
| |
| // Chunk 1: continuation of 'c' plus a whole packet 'e'. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(10, 'd', kContFromPrevChunk) |
| .AddPacket(20, 'e') |
| .CopyIntoTraceBuffer(); |
| |
| // Read cycle 3: 'a' and 'b' were already consumed. We should get the |
| // reassembled fragmented packet ['c','d'] and then 'e'. No data loss. |
| trace_buffer()->BeginRead(); |
| uint32_t previous_packet_dropped = 0; |
| |
| ASSERT_THAT( |
| ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(10, 'c'), FakePacketFragment(10, 'd'))); |
| EXPECT_FALSE(previous_packet_dropped); |
| |
| ASSERT_THAT(ReadPacket(nullptr, &previous_packet_dropped), |
| ElementsAre(FakePacketFragment(20, 'e'))); |
| EXPECT_FALSE(previous_packet_dropped); |
| |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Whole-packet path: the consumer reads the only packet of a chunk, returns, |
| // and pauses (mimicking the kApproxBytesPerTask=32KB break taken by |
| // ReadBuffersIntoConsumer in tracing_service_impl.cc). The chunk's |
| // payload_avail is now 0 but it has not been erased to padding yet. A |
| // concurrent producer wrap then incorrectly counts it in bytes_overwritten. |
| TEST_F(TraceBufferV2Test, BytesOverwritten_ConsumedWholePacketCountedAsLost) { |
| ResetBuffer(4096); |
| |
| // chunkX: a single whole packet on sequence {1,1}. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(512 - 16, 'a') |
| .CopyIntoTraceBuffer(); |
| |
| // Read the packet but do NOT call ReadNextTracePacket again. After the call |
| // ChunkSeqReader::ConsumeFragment has set payload_avail=0 on chunkX, but |
| // EraseCurrentChunk has not run, so chunkX is still a non-padding chunk |
| // sitting in seq.chunks. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(512 - 16, 'a'))); |
| |
| // Sanity: the chunk has been credited as bytes_read; nothing overwritten |
| // yet. |
| EXPECT_EQ(512u, trace_buffer()->stats().bytes_read()); |
| EXPECT_EQ(0u, trace_buffer()->stats().bytes_overwritten()); |
| EXPECT_EQ(0u, trace_buffer()->stats().chunks_overwritten()); |
| |
| // Fill the buffer with chunks the consumer never reads, then write a chunk |
| // that wraps and clobbers chunkX along with the new fillers. |
| // wr_ goes 512 -> 1536 -> 2560 -> wraps to 0 to fit the 2048B chunk. |
| CreateChunk(ProducerID(1), WriterID(2), ChunkID(0)) |
| .AddPacket(1024 - 16, 'b') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(2), ChunkID(1)) |
| .AddPacket(1024 - 16, 'c') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(3), ChunkID(0)) |
| .AddPacket(2048 - 16, 'd') |
| .CopyIntoTraceBuffer(); |
| EXPECT_EQ(1u, trace_buffer()->stats().write_wrap_count()); |
| |
| // chunkX (offset 0, 512B) had no unread data when overwritten, so it must |
| // not contribute to bytes_overwritten or chunks_overwritten. Only chunkY |
| // and chunkZ (1024B each, never read) legitimately carry data loss. |
| EXPECT_EQ(2u, trace_buffer()->stats().chunks_overwritten()); |
| EXPECT_EQ(2048u, trace_buffer()->stats().bytes_overwritten()); |
| } |
| |
| // Reassembly path: a fragmented packet split across N chunks. After |
| // ReassembleFragmentedPacket succeeds, ConsumeFragment runs on every chunk |
| // (payload_avail -> 0 on each), but ChunkSeqReader's seq_iter_ is still on |
| // the kFragBegin chunk. A producer wrap before the consumer next iterates |
| // counts ALL N chunks as overwritten, even though no data was lost. |
| TEST_F(TraceBufferV2Test, |
| BytesOverwritten_ConsumedFragmentedPacketCountedAsLost) { |
| ResetBuffer(4096); |
| |
| // A fragmented packet split across 3 chunks of 512B each. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(512 - 16, 'a', kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(512 - 16, 'b', kContFromPrevChunk | kContOnNextChunk) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(512 - 16, 'c', kContFromPrevChunk) |
| .CopyIntoTraceBuffer(); |
| |
| // Reassemble + read the packet. All 3 chunks have payload_avail=0 after |
| // this, but none have been erased into padding chunks. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(512 - 16, 'a'), |
| FakePacketFragment(512 - 16, 'b'), |
| FakePacketFragment(512 - 16, 'c'))); |
| |
| EXPECT_EQ(3u * 512u, trace_buffer()->stats().bytes_read()); |
| EXPECT_EQ(0u, trace_buffer()->stats().bytes_overwritten()); |
| EXPECT_EQ(0u, trace_buffer()->stats().chunks_overwritten()); |
| |
| // Add one big chunk to take used_size_ near the end, then a chunk that |
| // forces a wrap and lands on chunks A, B, C. |
| CreateChunk(ProducerID(1), WriterID(2), ChunkID(0)) |
| .AddPacket(2048 - 16, 'd') |
| .CopyIntoTraceBuffer(); // wr_=3584 |
| CreateChunk(ProducerID(1), WriterID(3), ChunkID(0)) |
| .AddPacket(1536 - 16, 'e') |
| .CopyIntoTraceBuffer(); |
| EXPECT_EQ(1u, trace_buffer()->stats().write_wrap_count()); |
| |
| // None of A, B, C had unread data when they were overwritten (the consumer |
| // already read the reassembled packet). Hence no chunks should be reported |
| // as overwritten and bytes_overwritten should be 0. |
| EXPECT_EQ(0u, trace_buffer()->stats().chunks_overwritten()); |
| EXPECT_EQ(0u, trace_buffer()->stats().bytes_overwritten()); |
| } |
| |
| // Same bug as ConsumedWholePacketCountedAsLost, but the cleanup gap stretches |
| // across a BeginRead() boundary — mimicking the consumer flow where |
| // ReadBuffersIntoConsumer (tracing_service_impl.cc:2507) yields after |
| // ~kApproxBytesPerTask=32KB of packet bytes and the next IPC task starts a |
| // fresh BeginRead. BeginRead does chunk_seq_reader_.reset(), so the |
| // "consumed-but-not-padded" chunk survives across reads until the new |
| // buffer-order walk happens to step on it again. If a producer wraps in |
| // between, bytes_overwritten still counts the chunk. |
| TEST_F(TraceBufferV2Test, |
| BytesOverwritten_ConsumedChunkSurvivesBeginReadBoundary) { |
| ResetBuffer(4096); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(512 - 16, 'a') |
| .CopyIntoTraceBuffer(); |
| |
| // First read cycle: reads the packet, then the consumer stops. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(512 - 16, 'a'))); |
| |
| // Second BeginRead before any cleanup happens. This resets |
| // chunk_seq_reader_ — the previously consumed chunkX is left in the |
| // buffer with payload_avail=0 and is_padding()==false, but no read |
| // iterator is keeping track of it. |
| trace_buffer()->BeginRead(); |
| |
| // Now a producer wrap arrives before the consumer's new walk reaches |
| // chunkX. Same arithmetic as the simple test: 512 + 1024 + 1024 = 2560, |
| // then a 2048B wrap clobbers the head. |
| CreateChunk(ProducerID(1), WriterID(2), ChunkID(0)) |
| .AddPacket(1024 - 16, 'b') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(2), ChunkID(1)) |
| .AddPacket(1024 - 16, 'c') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(3), ChunkID(0)) |
| .AddPacket(2048 - 16, 'd') |
| .CopyIntoTraceBuffer(); |
| EXPECT_EQ(1u, trace_buffer()->stats().write_wrap_count()); |
| |
| // chunkX was already drained before the BeginRead boundary. Only chunkY |
| // and chunkZ legitimately had unread data. |
| EXPECT_EQ(2u, trace_buffer()->stats().chunks_overwritten()); |
| EXPECT_EQ(2048u, trace_buffer()->stats().bytes_overwritten()); |
| } |
| |
| // A kFragBegin chunk pending a patch (kChunkNeedsPatching) whose patch never |
| // arrives gets evicted by a subsequent wrap. ReassembleFragmentedPacket |
| // returns kNotEnoughData on this chain and — unlike the kDataLoss branch — |
| // does NOT call ConsumeFragment on the frags it walked. As a result the |
| // kFragBegin chunk's data loss is not credited in bytes_overwritten / |
| // chunks_overwritten. The continuation chunkB is still credited because the |
| // outer DeleteNextChunksFor loop reaches it and the kFragEnd switch case in |
| // ReadNextPacketInSeqOrder calls ConsumeFragment directly. |
| TEST_F(TraceBufferV2Test, |
| BytesOverwritten_PendingPatchChunkOverwrittenIsCounted) { |
| ResetBuffer(4096); |
| |
| // chunkA: kFragBegin + kChunkNeedsPatching. Continuation chunkB exists, but |
| // the patch never arrives, so the consumer can't reassemble the packet. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(1024 - 16, 'a', kContOnNextChunk | kChunkNeedsPatching) |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(1024 - 16, 'b', kContFromPrevChunk) |
| .CopyIntoTraceBuffer(); |
| |
| // The reader can't deliver this fragmented packet because A needs a patch. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Wrap so chunkA and chunkB get evicted before the patch arrives. |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(0)) |
| .AddPacket(2048 - 16, 'c') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(3), WriterID(1), ChunkID(0)) |
| .AddPacket(2048 - 16, 'd') |
| .CopyIntoTraceBuffer(); |
| EXPECT_EQ(1u, trace_buffer()->stats().write_wrap_count()); |
| |
| // Both chunkA (kFragBegin pending patch) and chunkB (kFragEnd nobody read) |
| // carried unread data when overwritten. Both should be counted. |
| EXPECT_EQ(2u, trace_buffer()->stats().chunks_overwritten()); |
| EXPECT_EQ(2048u, trace_buffer()->stats().bytes_overwritten()); |
| } |
| |
| // chunks_overwritten / bytes_overwritten must not be incremented for a chunk |
| // that the reader already drained but hadn't yet "stepped past". The |
| // kFragWholePacket branch in ReadNextPacketInSeqOrder() decrements |
| // payload_avail to 0 and returns the packet to the consumer without erasing |
| // the chunk; the chunk only becomes a padding chunk on the next read call, |
| // when NextFragmentInChunk() returns nullopt and EraseCurrentChunk() runs. |
| // If a write forces a wrap in that gap, DeleteNextChunksFor() encounters a |
| // non-padding chunk and (incorrectly) bumps the overwrite counters, even |
| // though there is nothing left to lose. Mirrors NoDataLossIfReaderCatchesUp |
| // above, which exercises the same pattern but doesn't assert on the stats. |
| TEST_F(TraceBufferV2Test, NoOverwriteCountIfReaderCatchesUp) { |
| ResetBuffer(4096); |
| |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(2000, 'a') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(1)) |
| .AddPacket(1000, 'b') |
| .CopyIntoTraceBuffer(); |
| |
| // Read 'a' with a single ReadPacket(). Crucially, do not call ReadPacket() |
| // again — that follow-up call is what would turn ChunkID(0) into a padding |
| // chunk via EraseCurrentChunk(). |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(2000, 'a'))); |
| |
| // ChunkID(2) doesn't fit in the tail, so wr_ wraps to 0 and |
| // DeleteNextChunksFor() walks ChunkID(0). ChunkID(0) has no unread bytes, |
| // so this is not an overwrite. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(2)) |
| .AddPacket(2000, 'c') |
| .CopyIntoTraceBuffer(); |
| |
| EXPECT_EQ(0u, trace_buffer()->stats().chunks_overwritten()); |
| EXPECT_EQ(0u, trace_buffer()->stats().bytes_overwritten()); |
| |
| // Sanity check: the surviving packets are still readable and the consumer |
| // is not signalled any data loss — confirming that the increment above (if |
| // it fires) is bogus rather than a real overwrite reported elsewhere. |
| trace_buffer()->BeginRead(); |
| uint32_t dropped = 0; |
| ASSERT_THAT(ReadPacket(nullptr, &dropped), |
| ElementsAre(FakePacketFragment(1000, 'b'))); |
| EXPECT_FALSE(dropped); |
| ASSERT_THAT(ReadPacket(nullptr, &dropped), |
| ElementsAre(FakePacketFragment(2000, 'c'))); |
| EXPECT_FALSE(dropped); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // Scrape → read → recommit when the buffer is full. The producer's complete |
| // commit arrives when wr_ has reached size_, so cached_size_to_end is 0 and |
| // the wrap path would otherwise evict the same kChunkIncomplete chunk we are |
| // about to recommit. The recommit must rewrite the chunk in place: the |
| // consumer must see only the new packets ('b' and 'c'), not the previously- |
| // drained 'a' a second time. |
| TEST_F(TraceBufferV2Test, Override_ReCommitIncompleteOnFullBuffer) { |
| ResetBuffer(4096); |
| |
| // Scrape chunk 0: [Whole 'a'] [kFragBegin 'b'] padded to 1024 bytes. The |
| // last fragment is dropped; only 'a' is visible. The full 1024-byte slot |
| // is reserved so the producer can later grow into it. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(10, 'b', kContOnNextChunk) |
| .PadTo(1024) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| |
| // Read 'a'. payload_avail → 0 but kChunkIncomplete + kReadMode keeps the |
| // chunk in seq.chunks (no EraseCurrentChunk). |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Fill the rest of the buffer with chunks on a different sequence. wr_ |
| // ends at 4096 (== size_). |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(1)) |
| .AddPacket(1024 - 16, 'x') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(2)) |
| .AddPacket(1024 - 16, 'y') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(3)) |
| .AddPacket(1024 - 16, 'z') |
| .CopyIntoTraceBuffer(); |
| |
| // Producer commits chunk 0 with the final payload from the same SMB slot. |
| // Bytes 0..21 are still 'a' (an SMB invariant: producers never rewrite |
| // already-written bytes). The recommit must be detected and written in |
| // place — not turned into a fresh write that would re-emit 'a'. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(10, 'b') |
| .AddPacket(100, 'c') |
| .PadTo(1024) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/true); |
| |
| // Expected: 'a' is not delivered again; 'b' and 'c' are the newly-recovered |
| // packets; 'x', 'y', 'z' from the filler sequence follow. No data loss is |
| // signalled. |
| trace_buffer()->BeginRead(); |
| uint32_t dropped = 0; |
| ASSERT_THAT(ReadPacket(nullptr, &dropped), |
| ElementsAre(FakePacketFragment(10, 'b'))); |
| EXPECT_FALSE(dropped); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(100, 'c'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(1024 - 16, 'x'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(1024 - 16, 'y'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(1024 - 16, 'z'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| // An incomplete chunk that was scraped and then fully drained by the reader |
| // leaves an "empty shell" sitting in the buffer (payload_avail=0 but |
| // kChunkIncomplete prevents it from being erased to padding). If the |
| // producer never commits the chunk and the buffer wraps over it, the chunk |
| // gets evicted by DeleteNextChunksFor. In that case there is nothing |
| // unconsumed in the chunk: every fragment that was visible has already been |
| // delivered to the consumer, so the eviction must NOT be reported as data |
| // loss for the sequence — neither in the overwrite stats (which would |
| // double-count fragments already credited as bytes_read) nor in |
| // previous_packet_dropped on the next packet of that sequence. |
| TEST_F(TraceBufferV2Test, ScrapeWithLateRecommitAfterRead) { |
| ResetBuffer(4096); |
| SuppressClientDchecksForTesting(); |
| |
| // Scrape chunk 0 of seq A. The kFragBegin 'c' gets dropped by the scrape |
| // (last fragment of an incomplete chunk, plus kContOnNextChunk is cleared). |
| // Visible payload: [a, b]. The chunk is padded to 512B so the buffer can |
| // accept further commits in this slot. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .AddPacket(10, 'c') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(/*chunk_complete=*/false); |
| |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(1)) |
| .AddPacket(1024 - 16, 'd') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(2)) |
| .AddPacket(1024 - 16, 'e') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(3)) |
| .AddPacket(1024 - 16, 'f') |
| .CopyIntoTraceBuffer(); |
| |
| // Drain the visible packets. After this the chunk has payload_avail=0 but |
| // is NOT erased to padding because kChunkIncomplete + kReadMode skips the |
| // EraseCurrentChunk step in ReadNextPacketInSeqOrder. The "empty shell" is |
| // now sitting in the buffer. |
| trace_buffer()->BeginRead(); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(20, 'a'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(30, 'b'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(1024 - 16, 'd'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(1024 - 16, 'e'))); |
| ASSERT_THAT(ReadPacket(), ElementsAre(FakePacketFragment(1024 - 16, 'f'))); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| ASSERT_EQ(0u, trace_buffer()->stats().chunks_overwritten()); |
| |
| // Now wrap and overwrite. The overwrite should not trigger any data loss |
| // because all the chunks were consumed and empty. |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(4)) |
| .AddPacket(1024 - 16, 'g') |
| .CopyIntoTraceBuffer(); |
| CreateChunk(ProducerID(2), WriterID(1), ChunkID(5)) |
| .AddPacket(1024 - 16, 'h') |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| TraceBuffer::PacketSequenceProperties psp{}; |
| uint32_t dropped = 0; |
| ASSERT_THAT(ReadPacket(&psp, &dropped), |
| ElementsAre(FakePacketFragment(1024 - 16, 'g'))); |
| ASSERT_FALSE(dropped); |
| |
| ASSERT_THAT(ReadPacket(&psp, &dropped), |
| ElementsAre(FakePacketFragment(1024 - 16, 'h'))); |
| ASSERT_FALSE(dropped); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| |
| // Now commit the chunk long time after it has been read. We should still |
| // remember where we were left and only read the last fragment. |
| CreateChunk(ProducerID(1), WriterID(1), ChunkID(0)) |
| .AddPacket(20, 'a') |
| .AddPacket(30, 'b') |
| .AddPacket(10, 'c') |
| .PadTo(512) |
| .CopyIntoTraceBuffer(); |
| trace_buffer()->BeginRead(); |
| dropped = false; |
| ASSERT_THAT(ReadPacket(&psp, &dropped), |
| ElementsAre(FakePacketFragment(10, 'c'))); |
| ASSERT_FALSE(dropped); |
| ASSERT_THAT(ReadPacket(), IsEmpty()); |
| } |
| |
| } // namespace perfetto |