| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include "src/tracing/core/trace_writer_impl.h" |
| |
| #include <vector> |
| |
| #include "perfetto/ext/base/utils.h" |
| #include "perfetto/ext/tracing/core/commit_data_request.h" |
| #include "perfetto/ext/tracing/core/shared_memory_abi.h" |
| #include "perfetto/ext/tracing/core/trace_writer.h" |
| #include "perfetto/ext/tracing/core/tracing_service.h" |
| #include "perfetto/protozero/message.h" |
| #include "perfetto/protozero/proto_utils.h" |
| #include "perfetto/protozero/scattered_stream_writer.h" |
| #include "src/base/test/gtest_test_suite.h" |
| #include "src/base/test/test_task_runner.h" |
| #include "src/tracing/core/shared_memory_arbiter_impl.h" |
| #include "src/tracing/test/aligned_buffer_test.h" |
| #include "src/tracing/test/mock_producer_endpoint.h" |
| #include "test/gtest_and_gmock.h" |
| |
| #include "protos/perfetto/trace/test_event.gen.h" |
| #include "protos/perfetto/trace/test_event.pbzero.h" |
| #include "protos/perfetto/trace/trace_packet.gen.h" |
| #include "protos/perfetto/trace/trace_packet.pbzero.h" |
| |
| namespace perfetto { |
| namespace { |
| |
| using ChunkHeader = SharedMemoryABI::ChunkHeader; |
| using ::protozero::ScatteredStreamWriter; |
| using ::testing::AllOf; |
| using ::testing::ElementsAre; |
| using ::testing::IsNull; |
| using ::testing::MockFunction; |
| using ::testing::Ne; |
| using ::testing::NiceMock; |
| using ::testing::NotNull; |
| using ::testing::SizeIs; |
| using ::testing::ValuesIn; |
| |
| class TraceWriterImplTest : public AlignedBufferTest { |
| public: |
| struct PatchKey { |
| uint32_t writer_id; |
| uint32_t chunk_id; |
| bool operator<(const PatchKey& other) const { |
| return std::tie(writer_id, chunk_id) < |
| std::tie(other.writer_id, other.chunk_id); |
| } |
| }; |
| void SetUp() override { |
| default_layout_ = |
| SharedMemoryArbiterImpl::default_page_layout_for_testing(); |
| SharedMemoryArbiterImpl::set_default_layout_for_testing( |
| SharedMemoryABI::PageLayout::kPageDiv4); |
| AlignedBufferTest::SetUp(); |
| task_runner_.reset(new base::TestTaskRunner()); |
| arbiter_.reset(new SharedMemoryArbiterImpl(buf(), buf_size(), page_size(), |
| &mock_producer_endpoint_, |
| task_runner_.get())); |
| ON_CALL(mock_producer_endpoint_, CommitData) |
| .WillByDefault([&](const CommitDataRequest& req, |
| MockProducerEndpoint::CommitDataCallback cb) { |
| last_commit_ = req; |
| last_commit_callback_ = cb; |
| for (const CommitDataRequest::ChunkToPatch& c : |
| req.chunks_to_patch()) { |
| patches_[PatchKey{c.writer_id(), c.chunk_id()}] = c.patches(); |
| } |
| }); |
| } |
| |
| void TearDown() override { |
| arbiter_.reset(); |
| task_runner_.reset(); |
| SharedMemoryArbiterImpl::set_default_layout_for_testing(default_layout_); |
| } |
| |
| std::vector<uint8_t> CopyPayloadAndApplyPatches( |
| SharedMemoryABI::Chunk& chunk) const { |
| std::vector<uint8_t> copy(chunk.payload_begin(), |
| chunk.payload_begin() + chunk.payload_size()); |
| ChunkHeader::Packets p = chunk.header()->packets.load(); |
| |
| auto it = patches_.find(PatchKey{chunk.header()->writer_id.load(), |
| chunk.header()->chunk_id.load()}); |
| if (it == patches_.end()) { |
| EXPECT_FALSE(p.flags & ChunkHeader::kChunkNeedsPatching); |
| return copy; |
| } |
| EXPECT_TRUE(p.flags & ChunkHeader::kChunkNeedsPatching); |
| |
| for (const CommitDataRequest::ChunkToPatch::Patch& patch : it->second) { |
| if (patch.offset() + patch.data().size() > copy.size()) { |
| ADD_FAILURE() << "Patch out of bounds"; |
| continue; |
| } |
| for (size_t i = 0; i < patch.data().size(); i++) { |
| copy[patch.offset() + i] = |
| reinterpret_cast<const uint8_t*>(patch.data().data())[i]; |
| } |
| } |
| return copy; |
| } |
| |
| // Extracts trace packets from the shared memory buffer, and returns copies of |
| // them (after applying the patches received). The producer that writes to the |
| // shared memory (i.e. the trace writer) must be destroyed. |
| std::vector<std::string> GetPacketsFromShmemAndPatches() { |
| std::vector<std::string> packets; |
| SharedMemoryABI* abi = arbiter_->shmem_abi_for_testing(); |
| bool was_fragmenting = false; |
| for (size_t page_idx = 0; page_idx < abi->num_pages(); page_idx++) { |
| uint32_t page_layout = abi->GetPageLayout(page_idx); |
| size_t num_chunks = SharedMemoryABI::GetNumChunksForLayout(page_layout); |
| for (size_t chunk_idx = 0; chunk_idx < num_chunks; chunk_idx++) { |
| SharedMemoryABI::ChunkState chunk_state = |
| abi->GetChunkState(page_idx, chunk_idx); |
| if (chunk_state != SharedMemoryABI::kChunkFree && |
| chunk_state != SharedMemoryABI::kChunkComplete) { |
| ADD_FAILURE() << "Page " << page_idx << " chunk " << chunk_idx |
| << " unexpected state: " << chunk_state; |
| continue; |
| } |
| SharedMemoryABI::Chunk chunk = |
| abi->TryAcquireChunkForReading(page_idx, chunk_idx); |
| if (!chunk.is_valid()) |
| continue; |
| ChunkHeader::Packets p = chunk.header()->packets.load(); |
| |
| EXPECT_EQ( |
| was_fragmenting, |
| static_cast<bool>(p.flags & |
| ChunkHeader::kFirstPacketContinuesFromPrevChunk)); |
| |
| std::vector<uint8_t> payload = CopyPayloadAndApplyPatches(chunk); |
| |
| const uint8_t* read_ptr = payload.data(); |
| const uint8_t* const end_read_ptr = payload.data() + payload.size(); |
| |
| size_t num_fragments = p.count; |
| for (; num_fragments && read_ptr < end_read_ptr; num_fragments--) { |
| uint64_t len; |
| read_ptr = |
| protozero::proto_utils::ParseVarInt(read_ptr, end_read_ptr, &len); |
| if (!was_fragmenting || packets.empty()) { |
| packets.push_back(std::string()); |
| } |
| was_fragmenting = false; |
| if (read_ptr + len > end_read_ptr) { |
| ADD_FAILURE() << "Page " << page_idx << " chunk " << chunk_idx |
| << " malformed chunk"; |
| } |
| packets.back().append(reinterpret_cast<const char*>(read_ptr), |
| static_cast<size_t>(len)); |
| read_ptr += len; |
| } |
| EXPECT_EQ(num_fragments, 0u); |
| was_fragmenting = |
| p.flags & ChunkHeader::kLastPacketContinuesOnNextChunk; |
| } |
| } |
| return packets; |
| } |
| |
| SharedMemoryABI::PageLayout default_layout_; |
| CommitDataRequest last_commit_; |
| ProducerEndpoint::CommitDataCallback last_commit_callback_; |
| std::map<PatchKey, std::vector<CommitDataRequest::ChunkToPatch::Patch>> |
| patches_; |
| NiceMock<MockProducerEndpoint> mock_producer_endpoint_; |
| |
| std::unique_ptr<base::TestTaskRunner> task_runner_; |
| std::unique_ptr<SharedMemoryArbiterImpl> arbiter_; |
| }; |
| |
| size_t const kPageSizes[] = {4096, 65536}; |
| INSTANTIATE_TEST_SUITE_P(PageSize, TraceWriterImplTest, ValuesIn(kPageSizes)); |
| |
| TEST_P(TraceWriterImplTest, NewTracePacket) { |
| const BufferID kBufId = 42; |
| std::unique_ptr<TraceWriter> writer = arbiter_->CreateTraceWriter(kBufId); |
| const size_t kNumPackets = 32; |
| for (size_t i = 0; i < kNumPackets; i++) { |
| auto packet = writer->NewTracePacket(); |
| packet->set_for_testing()->set_str( |
| std::string("foobar " + std::to_string(i))); |
| } |
| |
| // Destroying the TraceWriteImpl should cause the last packet to be finalized |
| // and the chunk to be put back in the kChunkComplete state. |
| writer.reset(); |
| |
| std::vector<std::string> packets = GetPacketsFromShmemAndPatches(); |
| ASSERT_THAT(packets, SizeIs(kNumPackets)); |
| for (size_t i = 0; i < kNumPackets; i++) { |
| protos::gen::TracePacket packet; |
| EXPECT_TRUE(packet.ParseFromString(packets[i])); |
| EXPECT_EQ(packet.for_testing().str(), "foobar " + std::to_string(i)); |
| if (i == 0) { |
| EXPECT_TRUE(packet.first_packet_on_sequence()); |
| } else { |
| EXPECT_FALSE(packet.first_packet_on_sequence()); |
| } |
| } |
| } |
| |
| TEST_P(TraceWriterImplTest, NewTracePacketLargePackets) { |
| const BufferID kBufId = 42; |
| const size_t chunk_size = page_size() / 4; |
| std::unique_ptr<TraceWriter> writer = arbiter_->CreateTraceWriter(kBufId); |
| { |
| auto packet = writer->NewTracePacket(); |
| packet->set_for_testing()->set_str(std::string("PACKET_1") + |
| std::string(chunk_size, 'x')); |
| } |
| { |
| auto packet = writer->NewTracePacket(); |
| packet->set_for_testing()->set_str(std::string("PACKET_2") + |
| std::string(chunk_size, 'x')); |
| } |
| |
| // Destroying the TraceWriteImpl should cause the last packet to be finalized |
| // and the chunk to be put back in the kChunkComplete state. |
| writer.reset(); |
| |
| std::vector<std::string> packets = GetPacketsFromShmemAndPatches(); |
| ASSERT_THAT(packets, SizeIs(2)); |
| { |
| protos::gen::TracePacket packet; |
| EXPECT_TRUE(packet.ParseFromString(packets[0])); |
| EXPECT_EQ(packet.for_testing().str(), |
| std::string("PACKET_1") + std::string(chunk_size, 'x')); |
| } |
| { |
| protos::gen::TracePacket packet; |
| EXPECT_TRUE(packet.ParseFromString(packets[1])); |
| EXPECT_EQ(packet.for_testing().str(), |
| std::string("PACKET_2") + std::string(chunk_size, 'x')); |
| } |
| } |
| |
| // A prefix corresponding to first_packet_on_sequence = true in a serialized |
| // TracePacket proto. |
| constexpr char kFirstPacketOnSequenceFlagPrefix[] = {static_cast<char>(0xB8), |
| 0x5, 0x1, 0x0}; |
| |
| TEST_P(TraceWriterImplTest, NewTracePacketTakeWriter) { |
| const BufferID kBufId = 42; |
| std::unique_ptr<TraceWriter> writer = arbiter_->CreateTraceWriter(kBufId); |
| const size_t kNumPackets = 32; |
| for (size_t i = 0; i < kNumPackets; i++) { |
| ScatteredStreamWriter* sw = writer->NewTracePacket().TakeStreamWriter(); |
| std::string raw_proto_bytes = |
| std::string("RAW_PROTO_BYTES_") + std::to_string(i); |
| sw->WriteBytes(reinterpret_cast<const uint8_t*>(raw_proto_bytes.data()), |
| raw_proto_bytes.size()); |
| writer->FinishTracePacket(); |
| } |
| |
| // Destroying the TraceWriteImpl should cause the last packet to be finalized |
| // and the chunk to be put back in the kChunkComplete state. |
| writer.reset(); |
| |
| std::vector<std::string> packets = GetPacketsFromShmemAndPatches(); |
| ASSERT_THAT(packets, SizeIs(kNumPackets)); |
| for (size_t i = 0; i < kNumPackets; i++) { |
| std::string expected = "RAW_PROTO_BYTES_" + std::to_string(i); |
| if (i == 0) { |
| expected = kFirstPacketOnSequenceFlagPrefix + expected; |
| } |
| EXPECT_EQ(packets[i], expected); |
| } |
| } |
| |
| #if defined(GTEST_HAS_DEATH_TEST) |
| using TraceWriterImplDeathTest = TraceWriterImplTest; |
| INSTANTIATE_TEST_SUITE_P(PageSize, |
| TraceWriterImplDeathTest, |
| ValuesIn(kPageSizes)); |
| |
| TEST_P(TraceWriterImplDeathTest, NewTracePacketTakeWriterNoFinish) { |
| const BufferID kBufId = 42; |
| std::unique_ptr<TraceWriter> writer = arbiter_->CreateTraceWriter(kBufId); |
| |
| ScatteredStreamWriter* sw = writer->NewTracePacket().TakeStreamWriter(); |
| std::string raw_proto_bytes = std::string("RAW_PROTO_BYTES"); |
| sw->WriteBytes(reinterpret_cast<const uint8_t*>(raw_proto_bytes.data()), |
| raw_proto_bytes.size()); |
| |
| EXPECT_DEATH({ writer->NewTracePacket(); }, ""); |
| } |
| #endif // defined(GTEST_HAS_DEATH_TEST) |
| |
| TEST_P(TraceWriterImplTest, AnnotatePatch) { |
| const BufferID kBufId = 42; |
| std::unique_ptr<TraceWriter> writer = arbiter_->CreateTraceWriter(kBufId); |
| ScatteredStreamWriter* sw = writer->NewTracePacket().TakeStreamWriter(); |
| std::string raw_proto_bytes = std::string("RAW_PROTO_BYTES"); |
| sw->WriteBytes(reinterpret_cast<const uint8_t*>(raw_proto_bytes.data()), |
| raw_proto_bytes.size()); |
| |
| uint8_t* patch1 = |
| sw->ReserveBytes(ScatteredStreamWriter::Delegate::kPatchSize); |
| ASSERT_THAT(patch1, NotNull()); |
| patch1[0] = 0; |
| patch1[1] = 0; |
| patch1[2] = 0; |
| patch1[3] = 0; |
| const uint8_t* old_chunk_pointer = patch1; |
| patch1 = sw->AnnotatePatch(patch1); |
| EXPECT_NE(patch1, old_chunk_pointer); |
| ASSERT_THAT(patch1, NotNull()); |
| |
| sw->WriteByte('X'); |
| |
| uint8_t* patch2 = |
| sw->ReserveBytes(ScatteredStreamWriter::Delegate::kPatchSize); |
| ASSERT_THAT(patch2, NotNull()); |
| patch2[0] = 0; |
| patch2[1] = 0; |
| patch2[2] = 0; |
| patch2[3] = 0; |
| old_chunk_pointer = patch2; |
| patch2 = sw->AnnotatePatch(patch2); |
| EXPECT_NE(patch2, old_chunk_pointer); |
| ASSERT_THAT(patch2, NotNull()); |
| |
| const size_t chunk_size = page_size() / 4; |
| std::string large_string(chunk_size, 'x'); |
| |
| sw->WriteBytes(reinterpret_cast<const uint8_t*>(large_string.data()), |
| large_string.size()); |
| |
| uint8_t* patch3 = |
| sw->ReserveBytes(ScatteredStreamWriter::Delegate::kPatchSize); |
| ASSERT_THAT(patch3, NotNull()); |
| patch3[0] = 0; |
| patch3[1] = 0; |
| patch3[2] = 0; |
| patch3[3] = 0; |
| old_chunk_pointer = patch3; |
| patch3 = sw->AnnotatePatch(patch3); |
| EXPECT_NE(patch3, old_chunk_pointer); |
| ASSERT_THAT(patch3, NotNull()); |
| |
| sw->WriteBytes(reinterpret_cast<const uint8_t*>(large_string.data()), |
| large_string.size()); |
| |
| patch1[0] = 0x11; |
| patch1[1] = 0x11; |
| patch1[2] = 0x11; |
| patch1[3] = 0x11; |
| |
| patch2[0] = 0x22; |
| patch2[1] = 0x22; |
| patch2[2] = 0x22; |
| patch2[3] = 0x22; |
| |
| patch3[0] = 0x33; |
| patch3[1] = 0x33; |
| patch3[2] = 0x33; |
| patch3[3] = 0x33; |
| |
| writer->FinishTracePacket(); |
| |
| // Destroying the TraceWriteImpl should cause the last packet to be finalized |
| // and the chunk to be put back in the kChunkComplete state. |
| writer.reset(); |
| |
| std::vector<std::string> packets = GetPacketsFromShmemAndPatches(); |
| EXPECT_THAT( |
| packets, |
| ElementsAre( |
| kFirstPacketOnSequenceFlagPrefix + std::string("RAW_PROTO_BYTES") + |
| std::string("\x11\x11\x11\x11") + std::string("X") + |
| std::string("\x22\x22\x22\x22") + std::string(chunk_size, 'x') + |
| std::string("\x33\x33\x33\x33") + std::string(chunk_size, 'x'))); |
| } |
| |
| TEST_P(TraceWriterImplTest, MixManualTakeAndMessage) { |
| const BufferID kBufId = 42; |
| const size_t chunk_size = page_size() / 4; |
| const std::string large_string(chunk_size, 'x'); |
| |
| std::unique_ptr<TraceWriter> writer = arbiter_->CreateTraceWriter(kBufId); |
| |
| { |
| ScatteredStreamWriter* sw = writer->NewTracePacket().TakeStreamWriter(); |
| std::string packet1 = std::string("PACKET_1_"); |
| sw->WriteBytes(reinterpret_cast<const uint8_t*>(packet1.data()), |
| packet1.size()); |
| uint8_t* patch = |
| sw->ReserveBytes(ScatteredStreamWriter::Delegate::kPatchSize); |
| ASSERT_THAT(patch, NotNull()); |
| patch[0] = 0; |
| patch[1] = 0; |
| patch[2] = 0; |
| patch[3] = 0; |
| const uint8_t* old_chunk_pointer = patch; |
| patch = sw->AnnotatePatch(patch); |
| EXPECT_NE(patch, old_chunk_pointer); |
| ASSERT_THAT(patch, NotNull()); |
| sw->WriteBytes(reinterpret_cast<const uint8_t*>(large_string.data()), |
| large_string.size()); |
| patch[0] = 0xFF; |
| patch[1] = 0xFF; |
| patch[2] = 0xFF; |
| patch[3] = 0xFF; |
| writer->FinishTracePacket(); |
| } |
| |
| { |
| auto msg = writer->NewTracePacket(); |
| std::string packet2 = std::string("PACKET_2_"); |
| msg->AppendRawProtoBytes(packet2.data(), packet2.size()); |
| auto* nested = msg->BeginNestedMessage<protozero::Message>(1); |
| nested->AppendRawProtoBytes(large_string.data(), large_string.size()); |
| } |
| |
| { |
| ScatteredStreamWriter* sw = writer->NewTracePacket().TakeStreamWriter(); |
| std::string packet3 = std::string("PACKET_3_"); |
| sw->WriteBytes(reinterpret_cast<const uint8_t*>(packet3.data()), |
| packet3.size()); |
| uint8_t* patch = |
| sw->ReserveBytes(ScatteredStreamWriter::Delegate::kPatchSize); |
| ASSERT_THAT(patch, NotNull()); |
| patch[0] = 0; |
| patch[1] = 0; |
| patch[2] = 0; |
| patch[3] = 0; |
| const uint8_t* old_chunk_pointer = patch; |
| patch = sw->AnnotatePatch(patch); |
| EXPECT_NE(patch, old_chunk_pointer); |
| ASSERT_THAT(patch, NotNull()); |
| sw->WriteBytes(reinterpret_cast<const uint8_t*>(large_string.data()), |
| large_string.size()); |
| patch[0] = 0xFF; |
| patch[1] = 0xFF; |
| patch[2] = 0xFF; |
| patch[3] = 0xFF; |
| writer->FinishTracePacket(); |
| } |
| |
| // Destroying the TraceWriteImpl should cause the last packet to be finalized |
| // and the chunk to be put back in the kChunkComplete state. |
| writer.reset(); |
| |
| uint8_t buf[protozero::proto_utils::kMessageLengthFieldSize]; |
| protozero::proto_utils::WriteRedundantVarInt( |
| static_cast<uint32_t>(large_string.size()), buf, |
| protozero::proto_utils::kMessageLengthFieldSize); |
| std::string encoded_size(reinterpret_cast<char*>(buf), sizeof(buf)); |
| |
| std::vector<std::string> packets = GetPacketsFromShmemAndPatches(); |
| EXPECT_THAT( |
| packets, |
| ElementsAre(kFirstPacketOnSequenceFlagPrefix + std::string("PACKET_1_") + |
| std::string("\xFF\xFF\xFF\xFF") + |
| std::string(chunk_size, 'x'), |
| std::string("PACKET_2_") + std::string("\x0A") + |
| encoded_size + std::string(chunk_size, 'x'), |
| std::string("PACKET_3_") + std::string("\xFF\xFF\xFF\xFF") + |
| std::string(chunk_size, 'x'))); |
| } |
| |
| TEST_P(TraceWriterImplTest, FragmentingPacketWithProducerAndServicePatching) { |
| const BufferID kBufId = 42; |
| std::unique_ptr<TraceWriter> writer = arbiter_->CreateTraceWriter(kBufId); |
| |
| // Write a packet that's guaranteed to span more than a single chunk, but |
| // less than two chunks. |
| auto packet = writer->NewTracePacket(); |
| size_t chunk_size = page_size() / 4; |
| std::string large_string(chunk_size, 'x'); |
| packet->set_for_testing()->set_str(large_string); |
| |
| // First chunk should be committed. |
| arbiter_->FlushPendingCommitDataRequests(); |
| ASSERT_THAT(last_commit_.chunks_to_move(), SizeIs(1)); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].page(), 0u); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].chunk(), 0u); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].target_buffer(), kBufId); |
| EXPECT_THAT(last_commit_.chunks_to_patch(), SizeIs(0)); |
| |
| // We will simulate a batching cycle by first setting the batching period to |
| // a very large value and then force-flushing when we are done writing data. |
| arbiter_->SetDirectSMBPatchingSupportedByService(); |
| ASSERT_TRUE(arbiter_->EnableDirectSMBPatching()); |
| arbiter_->SetBatchCommitsDuration(UINT32_MAX); |
| |
| // Write a second packet that's guaranteed to span more than a single chunk. |
| // Starting a new trace packet should cause the patches for the first packet |
| // (i.e. for the first chunk) to be queued for sending to the service. They |
| // cannot be applied locally because the first chunk was already committed. |
| packet->Finalize(); |
| auto packet2 = writer->NewTracePacket(); |
| packet2->set_for_testing()->set_str(large_string); |
| |
| // Starting a new packet yet again should cause the patches for the second |
| // packet (i.e. for the second chunk) to be applied in the producer, because |
| // the second chunk has not been committed yet. |
| packet2->Finalize(); |
| auto packet3 = writer->NewTracePacket(); |
| |
| // Simulate the end of the batching period, which should trigger a commit to |
| // the service. |
| arbiter_->FlushPendingCommitDataRequests(); |
| |
| SharedMemoryABI* abi = arbiter_->shmem_abi_for_testing(); |
| |
| // The first allocated chunk should be complete but need patching, since the |
| // packet extended past the chunk and no patches for the packet size or |
| // string field size were applied yet. |
| ASSERT_EQ(abi->GetChunkState(0u, 0u), SharedMemoryABI::kChunkComplete); |
| auto chunk = abi->TryAcquireChunkForReading(0u, 0u); |
| ASSERT_TRUE(chunk.is_valid()); |
| EXPECT_EQ(chunk.header()->packets.load().count, 1u); |
| EXPECT_TRUE(chunk.header()->packets.load().flags & |
| SharedMemoryABI::ChunkHeader::kChunkNeedsPatching); |
| EXPECT_TRUE(chunk.header()->packets.load().flags & |
| SharedMemoryABI::ChunkHeader::kLastPacketContinuesOnNextChunk); |
| |
| // Verify that a patch for the first chunk was sent to the service. |
| ASSERT_THAT(last_commit_.chunks_to_patch(), SizeIs(1)); |
| EXPECT_EQ(last_commit_.chunks_to_patch()[0].writer_id(), writer->writer_id()); |
| EXPECT_EQ(last_commit_.chunks_to_patch()[0].target_buffer(), kBufId); |
| EXPECT_EQ(last_commit_.chunks_to_patch()[0].chunk_id(), |
| chunk.header()->chunk_id.load()); |
| EXPECT_FALSE(last_commit_.chunks_to_patch()[0].has_more_patches()); |
| EXPECT_THAT(last_commit_.chunks_to_patch()[0].patches(), SizeIs(1)); |
| |
| // Verify that the second chunk was committed. |
| ASSERT_THAT(last_commit_.chunks_to_move(), SizeIs(1)); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].page(), 0u); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].chunk(), 1u); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].target_buffer(), kBufId); |
| |
| // The second chunk should be in a complete state and should not need |
| // patching, as the patches to it should have been applied in the producer. |
| ASSERT_EQ(abi->GetChunkState(0u, 1u), SharedMemoryABI::kChunkComplete); |
| auto chunk2 = abi->TryAcquireChunkForReading(0u, 1u); |
| ASSERT_TRUE(chunk2.is_valid()); |
| EXPECT_EQ(chunk2.header()->packets.load().count, 2); |
| EXPECT_TRUE(chunk2.header()->packets.load().flags & |
| SharedMemoryABI::ChunkHeader::kLastPacketContinuesOnNextChunk); |
| EXPECT_FALSE(chunk2.header()->packets.load().flags & |
| SharedMemoryABI::ChunkHeader::kChunkNeedsPatching); |
| } |
| |
| TEST_P(TraceWriterImplTest, FragmentingPacketWithoutEnablingProducerPatching) { |
| // We will simulate a batching cycle by first setting the batching period to |
| // a very large value and will force flush to simulate a flush happening |
| // when we believe it should - in this case when a patch is encountered. |
| // |
| // Note: direct producer-side patching should be disabled by default. |
| arbiter_->SetBatchCommitsDuration(UINT32_MAX); |
| |
| const BufferID kBufId = 42; |
| std::unique_ptr<TraceWriter> writer = arbiter_->CreateTraceWriter(kBufId); |
| |
| // Write a packet that's guaranteed to span more than a single chunk. |
| auto packet = writer->NewTracePacket(); |
| size_t chunk_size = page_size() / 4; |
| std::string large_string(chunk_size, 'x'); |
| packet->set_for_testing()->set_str(large_string); |
| |
| // Starting a new packet should cause the first chunk and its patches to be |
| // committed to the service. |
| packet->Finalize(); |
| auto packet2 = writer->NewTracePacket(); |
| arbiter_->FlushPendingCommitDataRequests(); |
| |
| // The first allocated chunk should be complete but need patching, since the |
| // packet extended past the chunk and no patches for the packet size or |
| // string field size were applied in the producer. |
| SharedMemoryABI* abi = arbiter_->shmem_abi_for_testing(); |
| ASSERT_EQ(abi->GetChunkState(0u, 0u), SharedMemoryABI::kChunkComplete); |
| auto chunk = abi->TryAcquireChunkForReading(0u, 0u); |
| ASSERT_TRUE(chunk.is_valid()); |
| EXPECT_EQ(chunk.header()->packets.load().count, 1); |
| EXPECT_TRUE(chunk.header()->packets.load().flags & |
| SharedMemoryABI::ChunkHeader::kChunkNeedsPatching); |
| EXPECT_TRUE(chunk.header()->packets.load().flags & |
| SharedMemoryABI::ChunkHeader::kLastPacketContinuesOnNextChunk); |
| |
| // The first chunk was committed. |
| ASSERT_THAT(last_commit_.chunks_to_move(), SizeIs(1)); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].page(), 0u); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].chunk(), 0u); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].target_buffer(), kBufId); |
| |
| // The patches for the first chunk were committed. |
| ASSERT_THAT(last_commit_.chunks_to_patch(), SizeIs(1)); |
| EXPECT_EQ(last_commit_.chunks_to_patch()[0].writer_id(), writer->writer_id()); |
| EXPECT_EQ(last_commit_.chunks_to_patch()[0].target_buffer(), kBufId); |
| EXPECT_EQ(last_commit_.chunks_to_patch()[0].chunk_id(), |
| chunk.header()->chunk_id.load()); |
| EXPECT_FALSE(last_commit_.chunks_to_patch()[0].has_more_patches()); |
| EXPECT_THAT(last_commit_.chunks_to_patch()[0].patches(), SizeIs(1)); |
| } |
| |
| // Sets up a scenario in which the SMB is exhausted and TraceWriter fails to |
| // get a new chunk while fragmenting a packet. Verifies that data is dropped |
| // until the SMB is freed up and TraceWriter can get a new chunk. |
| TEST_P(TraceWriterImplTest, FragmentingPacketWhileBufferExhausted) { |
| const BufferID kBufId = 42; |
| std::unique_ptr<TraceWriter> writer = |
| arbiter_->CreateTraceWriter(kBufId, BufferExhaustedPolicy::kDrop); |
| |
| // Write a small first packet, so that |writer| owns a chunk. |
| auto packet = writer->NewTracePacket(); |
| EXPECT_FALSE(reinterpret_cast<TraceWriterImpl*>(writer.get()) |
| ->drop_packets_for_testing()); |
| // 3 bytes for the first_packet_on_sequence flag. |
| EXPECT_EQ(packet->Finalize(), 3u); |
| |
| // Grab all the remaining chunks in the SMB in new writers. |
| std::array<std::unique_ptr<TraceWriter>, kNumPages * 4 - 1> other_writers; |
| for (size_t i = 0; i < other_writers.size(); i++) { |
| other_writers[i] = |
| arbiter_->CreateTraceWriter(kBufId, BufferExhaustedPolicy::kDrop); |
| auto other_writer_packet = other_writers[i]->NewTracePacket(); |
| EXPECT_FALSE(reinterpret_cast<TraceWriterImpl*>(other_writers[i].get()) |
| ->drop_packets_for_testing()); |
| } |
| |
| // Write a packet that's guaranteed to span more than a single chunk, |
| // causing |writer| to attempt to acquire a new chunk but fail to do so. |
| auto packet2 = writer->NewTracePacket(); |
| size_t chunk_size = page_size() / 4; |
| std::string large_string(chunk_size, 'x'); |
| packet2->set_for_testing()->set_str(large_string); |
| |
| EXPECT_TRUE(reinterpret_cast<TraceWriterImpl*>(writer.get()) |
| ->drop_packets_for_testing()); |
| |
| // First chunk should be committed. |
| arbiter_->FlushPendingCommitDataRequests(); |
| ASSERT_THAT(last_commit_.chunks_to_move(), SizeIs(1)); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].page(), 0u); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].chunk(), 0u); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].target_buffer(), kBufId); |
| EXPECT_THAT(last_commit_.chunks_to_patch(), SizeIs(0)); |
| |
| // It should not need patching and not have the continuation flag set. |
| SharedMemoryABI* abi = arbiter_->shmem_abi_for_testing(); |
| ASSERT_EQ(SharedMemoryABI::kChunkComplete, abi->GetChunkState(0u, 0u)); |
| auto chunk = abi->TryAcquireChunkForReading(0u, 0u); |
| ASSERT_TRUE(chunk.is_valid()); |
| EXPECT_EQ(chunk.header()->packets.load().count, 2); |
| EXPECT_FALSE(chunk.header()->packets.load().flags & |
| SharedMemoryABI::ChunkHeader::kChunkNeedsPatching); |
| EXPECT_FALSE(chunk.header()->packets.load().flags & |
| SharedMemoryABI::ChunkHeader::kLastPacketContinuesOnNextChunk); |
| |
| // Writing more data while in garbage mode succeeds. This data is dropped. |
| packet2->Finalize(); |
| auto packet3 = writer->NewTracePacket(); |
| packet3->set_for_testing()->set_str(large_string); |
| |
| // Release the |writer|'s first chunk as free, so that it can grab it again. |
| abi->ReleaseChunkAsFree(std::move(chunk)); |
| |
| // Starting a new packet should cause TraceWriter to attempt to grab a new |
| // chunk again, because we wrote enough data to wrap the garbage chunk. |
| packet3->Finalize(); |
| auto packet4 = writer->NewTracePacket(); |
| |
| // Grabbing the chunk should have succeeded. |
| EXPECT_FALSE(reinterpret_cast<TraceWriterImpl*>(writer.get()) |
| ->drop_packets_for_testing()); |
| |
| // The first packet in the chunk should have the previous_packet_dropped |
| // flag set, so shouldn't be empty. |
| EXPECT_GT(packet4->Finalize(), 0u); |
| |
| // Flushing the writer causes the chunk to be released again. |
| writer->Flush(); |
| EXPECT_THAT(last_commit_.chunks_to_move(), SizeIs(1)); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].page(), 0u); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].chunk(), 0u); |
| EXPECT_THAT(last_commit_.chunks_to_patch(), SizeIs(0)); |
| |
| // Chunk should contain only |packet4| and not have any continuation flag |
| // set. |
| ASSERT_EQ(abi->GetChunkState(0u, 0u), SharedMemoryABI::kChunkComplete); |
| chunk = abi->TryAcquireChunkForReading(0u, 0u); |
| ASSERT_TRUE(chunk.is_valid()); |
| ASSERT_EQ(chunk.header()->packets.load().count, 1); |
| EXPECT_FALSE(chunk.header()->packets.load().flags & |
| SharedMemoryABI::ChunkHeader::kChunkNeedsPatching); |
| EXPECT_FALSE( |
| chunk.header()->packets.load().flags & |
| SharedMemoryABI::ChunkHeader::kFirstPacketContinuesFromPrevChunk); |
| EXPECT_FALSE(chunk.header()->packets.load().flags & |
| SharedMemoryABI::ChunkHeader::kLastPacketContinuesOnNextChunk); |
| } |
| |
| // Verifies that a TraceWriter that is flushed before the SMB is full and then |
| // acquires a garbage chunk later recovers and writes a |
| // previous_packet_dropped marker into the trace. |
| TEST_P(TraceWriterImplTest, FlushBeforeBufferExhausted) { |
| const BufferID kBufId = 42; |
| std::unique_ptr<TraceWriter> writer = |
| arbiter_->CreateTraceWriter(kBufId, BufferExhaustedPolicy::kDrop); |
| |
| // Write a small first packet and flush it, so that |writer| no longer owns |
| // any chunk. |
| auto packet = writer->NewTracePacket(); |
| EXPECT_FALSE(reinterpret_cast<TraceWriterImpl*>(writer.get()) |
| ->drop_packets_for_testing()); |
| // 3 bytes for the first_packet_on_sequence flag. |
| EXPECT_EQ(packet->Finalize(), 3u); |
| |
| // Flush the first chunk away. |
| writer->Flush(); |
| |
| // First chunk should be committed. Don't release it as free just yet. |
| arbiter_->FlushPendingCommitDataRequests(); |
| ASSERT_THAT(last_commit_.chunks_to_move(), SizeIs(1)); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].page(), 0u); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].chunk(), 0u); |
| |
| // Grab all the remaining chunks in the SMB in new writers. |
| std::array<std::unique_ptr<TraceWriter>, kNumPages * 4 - 1> other_writers; |
| for (size_t i = 0; i < other_writers.size(); i++) { |
| other_writers[i] = |
| arbiter_->CreateTraceWriter(kBufId, BufferExhaustedPolicy::kDrop); |
| auto other_writer_packet = other_writers[i]->NewTracePacket(); |
| EXPECT_FALSE(reinterpret_cast<TraceWriterImpl*>(other_writers[i].get()) |
| ->drop_packets_for_testing()); |
| } |
| |
| // Write another packet, causing |writer| to acquire a garbage chunk. |
| auto packet2 = writer->NewTracePacket(); |
| EXPECT_TRUE(reinterpret_cast<TraceWriterImpl*>(writer.get()) |
| ->drop_packets_for_testing()); |
| |
| // Writing more data while in garbage mode succeeds. This data is dropped. |
| // Make sure that we fill the garbage chunk, so that |writer| tries to |
| // re-acquire a valid chunk for the next packet. |
| size_t chunk_size = page_size() / 4; |
| std::string large_string(chunk_size, 'x'); |
| packet2->set_for_testing()->set_str(large_string); |
| packet2->Finalize(); |
| |
| // Next packet should still be in the garbage chunk. |
| auto packet3 = writer->NewTracePacket(); |
| EXPECT_TRUE(reinterpret_cast<TraceWriterImpl*>(writer.get()) |
| ->drop_packets_for_testing()); |
| |
| // Release the first chunk as free, so |writer| can acquire it again. |
| SharedMemoryABI* abi = arbiter_->shmem_abi_for_testing(); |
| ASSERT_EQ(SharedMemoryABI::kChunkComplete, abi->GetChunkState(0u, 0u)); |
| auto chunk = abi->TryAcquireChunkForReading(0u, 0u); |
| ASSERT_TRUE(chunk.is_valid()); |
| abi->ReleaseChunkAsFree(std::move(chunk)); |
| |
| // Fill the garbage chunk, so that the writer attempts to grab another chunk |
| // for |packet4|. |
| packet3->set_for_testing()->set_str(large_string); |
| packet3->Finalize(); |
| |
| // Next packet should go into the reacquired chunk we just released. |
| auto packet4 = writer->NewTracePacket(); |
| EXPECT_FALSE(reinterpret_cast<TraceWriterImpl*>(writer.get()) |
| ->drop_packets_for_testing()); |
| |
| // The first packet in the chunk should have the previous_packet_dropped |
| // flag set, so shouldn't be empty. |
| EXPECT_GT(packet4->Finalize(), 0u); |
| |
| // Flushing the writer causes the chunk to be released again. |
| writer->Flush(); |
| ASSERT_THAT(last_commit_.chunks_to_move(), SizeIs(1)); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].page(), 0u); |
| EXPECT_EQ(last_commit_.chunks_to_move()[0].chunk(), 0u); |
| EXPECT_THAT(last_commit_.chunks_to_patch(), SizeIs(0)); |
| |
| // Chunk should contain only |packet4| and not have any continuation flag |
| // set. |
| ASSERT_EQ(SharedMemoryABI::kChunkComplete, abi->GetChunkState(0u, 0u)); |
| chunk = abi->TryAcquireChunkForReading(0u, 0u); |
| ASSERT_TRUE(chunk.is_valid()); |
| ASSERT_EQ(chunk.header()->packets.load().count, 1); |
| ASSERT_FALSE(chunk.header()->packets.load().flags & |
| SharedMemoryABI::ChunkHeader::kChunkNeedsPatching); |
| ASSERT_FALSE( |
| chunk.header()->packets.load().flags & |
| SharedMemoryABI::ChunkHeader::kFirstPacketContinuesFromPrevChunk); |
| ASSERT_FALSE(chunk.header()->packets.load().flags & |
| SharedMemoryABI::ChunkHeader::kLastPacketContinuesOnNextChunk); |
| } |
| |
| // Regression test that verifies that flushing a TraceWriter while a |
| // fragmented packet still has uncommitted patches doesn't hit a DCHECK / |
| // crash the writer thread. |
| TEST_P(TraceWriterImplTest, FlushAfterFragmentingPacketWhileBufferExhausted) { |
| const BufferID kBufId = 42; |
| std::unique_ptr<TraceWriter> writer = |
| arbiter_->CreateTraceWriter(kBufId, BufferExhaustedPolicy::kDrop); |
| |
| // Write a small first packet, so that |writer| owns a chunk. |
| auto packet = writer->NewTracePacket(); |
| EXPECT_FALSE(reinterpret_cast<TraceWriterImpl*>(writer.get()) |
| ->drop_packets_for_testing()); |
| // 3 bytes for the first_packet_on_sequence flag. |
| EXPECT_EQ(packet->Finalize(), 3u); |
| |
| // Grab all but one of the remaining chunks in the SMB in new writers. |
| std::array<std::unique_ptr<TraceWriter>, kNumPages * 4 - 2> other_writers; |
| for (size_t i = 0; i < other_writers.size(); i++) { |
| other_writers[i] = |
| arbiter_->CreateTraceWriter(kBufId, BufferExhaustedPolicy::kDrop); |
| auto other_writer_packet = other_writers[i]->NewTracePacket(); |
| EXPECT_FALSE(reinterpret_cast<TraceWriterImpl*>(other_writers[i].get()) |
| ->drop_packets_for_testing()); |
| } |
| |
| // Write a packet that's guaranteed to span more than a two chunks, causing |
| // |writer| to attempt to acquire two new chunks, but fail to acquire the |
| // second. |
| auto packet2 = writer->NewTracePacket(); |
| size_t chunk_size = page_size() / 4; |
| std::string large_string(chunk_size * 2, 'x'); |
| packet2->set_for_testing()->set_str(large_string); |
| |
| EXPECT_TRUE(reinterpret_cast<TraceWriterImpl*>(writer.get()) |
| ->drop_packets_for_testing()); |
| |
| // First two chunks should be committed. |
| arbiter_->FlushPendingCommitDataRequests(); |
| ASSERT_THAT(last_commit_.chunks_to_move(), SizeIs(2)); |
| |
| // Flushing should succeed, even though some patches are still in the |
| // writer's patch list. |
| packet2->Finalize(); |
| writer->Flush(); |
| } |
| |
| TEST_P(TraceWriterImplTest, AnnotatePatchWhileBufferExhausted) { |
| const BufferID kBufId = 42; |
| std::unique_ptr<TraceWriter> writer = |
| arbiter_->CreateTraceWriter(kBufId, BufferExhaustedPolicy::kDrop); |
| |
| // Write a small first packet, so that |writer| owns a chunk. |
| ScatteredStreamWriter* sw = writer->NewTracePacket().TakeStreamWriter(); |
| sw->WriteBytes(reinterpret_cast<const uint8_t*>("X"), 1); |
| writer->FinishTracePacket(); |
| EXPECT_FALSE(reinterpret_cast<TraceWriterImpl*>(writer.get()) |
| ->drop_packets_for_testing()); |
| |
| // Grab all but one of the remaining chunks in the SMB in new writers. |
| std::array<std::unique_ptr<TraceWriter>, kNumPages * 4 - 2> other_writers; |
| for (size_t i = 0; i < other_writers.size(); i++) { |
| other_writers[i] = |
| arbiter_->CreateTraceWriter(kBufId, BufferExhaustedPolicy::kDrop); |
| auto other_writer_packet = other_writers[i]->NewTracePacket(); |
| EXPECT_FALSE(reinterpret_cast<TraceWriterImpl*>(other_writers[i].get()) |
| ->drop_packets_for_testing()); |
| } |
| |
| // Write a packet that's guaranteed to span more than a two chunks, causing |
| // |writer| to attempt to acquire two new chunks, but fail to acquire the |
| // second. |
| sw = writer->NewTracePacket().TakeStreamWriter(); |
| size_t chunk_size = page_size() / 4; |
| std::string large_string(chunk_size * 2, 'x'); |
| sw->WriteBytes(reinterpret_cast<const uint8_t*>(large_string.data()), |
| large_string.size()); |
| |
| EXPECT_TRUE(reinterpret_cast<TraceWriterImpl*>(writer.get()) |
| ->drop_packets_for_testing()); |
| |
| uint8_t* patch1 = |
| sw->ReserveBytes(ScatteredStreamWriter::Delegate::kPatchSize); |
| ASSERT_THAT(patch1, NotNull()); |
| patch1[0] = 0; |
| patch1[1] = 0; |
| patch1[2] = 0; |
| patch1[3] = 0; |
| patch1 = sw->AnnotatePatch(patch1); |
| EXPECT_THAT(patch1, IsNull()); |
| |
| // First two chunks should be committed. |
| arbiter_->FlushPendingCommitDataRequests(); |
| ASSERT_THAT(last_commit_.chunks_to_move(), SizeIs(2)); |
| |
| // Flushing should succeed, even though some patches are still in the |
| // writer's patch list. |
| writer->FinishTracePacket(); |
| writer->Flush(); |
| } |
| |
| TEST_P(TraceWriterImplTest, Flush) { |
| MockFunction<void()> flush_cb; |
| |
| const BufferID kBufId = 42; |
| std::unique_ptr<TraceWriter> writer = arbiter_->CreateTraceWriter(kBufId); |
| { |
| auto packet = writer->NewTracePacket(); |
| packet->set_for_testing()->set_str("foobar"); |
| } |
| |
| EXPECT_CALL(flush_cb, Call).Times(0); |
| ASSERT_FALSE(last_commit_callback_); |
| writer->Flush(flush_cb.AsStdFunction()); |
| ASSERT_TRUE(last_commit_callback_); |
| EXPECT_CALL(flush_cb, Call).Times(1); |
| last_commit_callback_(); |
| } |
| |
| TEST_P(TraceWriterImplTest, NestedMsgsPatches) { |
| const BufferID kBufId = 42; |
| const uint32_t kNestedFieldId = 1; |
| const uint32_t kStringFieldId = 2; |
| const uint32_t kIntFieldId = 3; |
| std::unique_ptr<TraceWriter> writer = arbiter_->CreateTraceWriter(kBufId); |
| |
| size_t chunk_size = page_size() / 4; |
| std::string large_string(chunk_size, 'x'); |
| |
| auto packet = writer->NewTracePacket(); |
| auto* nested1 = |
| packet->BeginNestedMessage<protozero::Message>(kNestedFieldId); |
| auto* nested2 = |
| nested1->BeginNestedMessage<protozero::Message>(kNestedFieldId); |
| auto* nested3 = |
| nested2->BeginNestedMessage<protozero::Message>(kNestedFieldId); |
| uint8_t* const old_nested_1_size_field = nested1->size_field(); |
| uint8_t* const old_nested_2_size_field = nested2->size_field(); |
| uint8_t* const old_nested_3_size_field = nested3->size_field(); |
| EXPECT_THAT(old_nested_1_size_field, NotNull()); |
| EXPECT_THAT(old_nested_2_size_field, NotNull()); |
| EXPECT_THAT(old_nested_3_size_field, NotNull()); |
| |
| // Append a small field, which will fit in the current chunk. |
| nested3->AppendVarInt<uint64_t>(kIntFieldId, 1); |
| |
| // The `size_field`s still point to the same old location, inside the chunk. |
| EXPECT_EQ(nested1->size_field(), old_nested_1_size_field); |
| EXPECT_EQ(nested2->size_field(), old_nested_2_size_field); |
| EXPECT_EQ(nested3->size_field(), old_nested_3_size_field); |
| |
| // Append a large string, which will not fit in the current chunk. |
| nested3->AppendString(kStringFieldId, large_string); |
| |
| // The `size_field`s will now point to different locations (patches). |
| EXPECT_THAT(nested1->size_field(), |
| AllOf(Ne(old_nested_1_size_field), NotNull())); |
| EXPECT_THAT(nested2->size_field(), |
| AllOf(Ne(old_nested_2_size_field), NotNull())); |
| EXPECT_THAT(nested3->size_field(), |
| AllOf(Ne(old_nested_3_size_field), NotNull())); |
| |
| packet->Finalize(); |
| writer->Flush(); |
| |
| arbiter_->FlushPendingCommitDataRequests(); |
| |
| ASSERT_THAT(last_commit_.chunks_to_patch(), SizeIs(1)); |
| EXPECT_EQ(last_commit_.chunks_to_patch()[0].writer_id(), writer->writer_id()); |
| EXPECT_EQ(last_commit_.chunks_to_patch()[0].target_buffer(), kBufId); |
| EXPECT_FALSE(last_commit_.chunks_to_patch()[0].has_more_patches()); |
| EXPECT_THAT(last_commit_.chunks_to_patch()[0].patches(), SizeIs(3)); |
| } |
| |
| // TODO(primiano): add multi-writer test. |
| |
| } // namespace |
| } // namespace perfetto |