@@ -2447,4 +2447,75 @@ TEST_F(TraceBufferV2Test, RescrapeAfterEviction_PartiallyRead) {
24472447 EXPECT_FALSE (found_a) << " Packet 'a' was re-read after eviction+rescrape" ;
24482448}
24492449
2450+ // Regression test for the scrape-recommit-read interaction. When a chunk is
2451+ // scraped (chunk_complete=false), it gets kChunkIncomplete. If the same chunk
2452+ // is scraped again with the same payload, the recommit path must NOT clear
2453+ // kChunkIncomplete. Otherwise the reader would consume it prematurely, and when
2454+ // the real IPC commit arrives it's discarded as "late", orphaning the kFragEnd
2455+ // in the next chunk.
2456+ TEST_F (TraceBufferV2Test, ScrapeRecommitPreservesIncomplete) {
2457+ ResetBuffer (4096 );
2458+ SuppressClientDchecksForTesting ();
2459+
2460+ // Scrape chunk 0: [Whole 'a'] [kFragBegin 'b' kContOnNextChunk].
2461+ // Scraping drops the last fragment ('b'), clears kContOnNextChunk, sets
2462+ // kChunkIncomplete. Only fragment 'a' is visible.
2463+ CreateChunk (ProducerID (1 ), WriterID (1 ), ChunkID (0 ))
2464+ .AddPacket (20 , ' a' )
2465+ .AddPacket (10 , ' b' , kContOnNextChunk )
2466+ .PadTo (512 )
2467+ .CopyIntoTraceBuffer (/* chunk_complete=*/ false );
2468+
2469+ // Read cycle 1: gets 'a'. The chunk's payload is fully consumed but it is
2470+ // NOT erased because kChunkIncomplete prevents it.
2471+ trace_buffer ()->BeginRead ();
2472+ ASSERT_THAT (ReadPacket (), ElementsAre (FakePacketFragment (20 , ' a' )));
2473+ ASSERT_THAT (ReadPacket (), IsEmpty ());
2474+
2475+ // Second scrape of the same chunk (same payload). This triggers the recommit
2476+ // path. The fix ensures kChunkIncomplete is NOT cleared here.
2477+ CreateChunk (ProducerID (1 ), WriterID (1 ), ChunkID (0 ))
2478+ .AddPacket (20 , ' a' )
2479+ .AddPacket (10 , ' b' , kContOnNextChunk )
2480+ .PadTo (512 )
2481+ .CopyIntoTraceBuffer (/* chunk_complete=*/ false );
2482+
2483+ // Read cycle 2: chunk still has kChunkIncomplete, so nothing new to read.
2484+ trace_buffer ()->BeginRead ();
2485+ ASSERT_THAT (ReadPacket (), IsEmpty ());
2486+
2487+ // The chunk must NOT have been consumed (no chunks_discarded bump yet).
2488+ EXPECT_EQ (0u , trace_buffer ()->stats ().chunks_discarded ());
2489+
2490+ // IPC recommit: producer finished writing, commits as complete. This clears
2491+ // kChunkIncomplete and restores the full payload including fragment 'b'.
2492+ CreateChunk (ProducerID (1 ), WriterID (1 ), ChunkID (0 ))
2493+ .AddPacket (20 , ' a' )
2494+ .AddPacket (10 , ' b' , kContOnNextChunk )
2495+ .PadTo (512 )
2496+ .CopyIntoTraceBuffer (/* chunk_complete=*/ true );
2497+
2498+ // Chunk 1: continuation of 'b' plus a whole packet 'd'.
2499+ CreateChunk (ProducerID (1 ), WriterID (1 ), ChunkID (1 ))
2500+ .AddPacket (10 , ' c' , kContFromPrevChunk )
2501+ .AddPacket (20 , ' d' )
2502+ .CopyIntoTraceBuffer ();
2503+
2504+ // Read cycle 3: 'a' was already consumed. We should get the reassembled
2505+ // fragmented packet ['b','c'] and then 'd'. No data loss.
2506+ trace_buffer ()->BeginRead ();
2507+ bool previous_packet_dropped = false ;
2508+
2509+ ASSERT_THAT (
2510+ ReadPacket (nullptr , &previous_packet_dropped),
2511+ ElementsAre (FakePacketFragment (10 , ' b' ), FakePacketFragment (10 , ' c' )));
2512+ EXPECT_FALSE (previous_packet_dropped);
2513+
2514+ ASSERT_THAT (ReadPacket (nullptr , &previous_packet_dropped),
2515+ ElementsAre (FakePacketFragment (20 , ' d' )));
2516+ EXPECT_FALSE (previous_packet_dropped);
2517+
2518+ ASSERT_THAT (ReadPacket (), IsEmpty ());
2519+ }
2520+
24502521} // namespace perfetto
0 commit comments