Skip to content

protozero: let transports read directly into ProtoRingBuffer - #7264

Open
LalitMaganti wants to merge 1 commit into
mainfrom
dev/lalitm/zerocopy-1-ringbuffer
Open

protozero: let transports read directly into ProtoRingBuffer#7264
LalitMaganti wants to merge 1 commit into
mainfrom
dev/lalitm/zerocopy-1-ringbuffer

Conversation

@LalitMaganti

@LalitMaganti LalitMaganti commented Aug 28, 2026

Copy link
Copy Markdown
Member

Every transport kept a staging buffer, read into it, then handed those
bytes to Append(), which copied them again. BeginWrite()/EndWrite() lets
read(2) deposit them in the ring buffer directly, so they are copied
once.

The reservation is handed out as a move-only WriteHandle that must be
consumed exactly once, by EndWrite() to keep what was written or
AbortWrite() to discard it; dropping one fails a CHECK. Holding it in a
value the compiler tracks is what makes the pattern safe to use: the
buffer refuses to recompact, grow or hand out a second reservation while
one is outstanding, so bytes cannot move under a pointer someone still
holds. Rpc::RequestHandle carries the same contract out to the
transports.

out/linux_clang_release/perfetto_benchmarks
--benchmark_filter='ProtoRingBufferIngest|ProtoRingBufferDispatch'
ingest, 4KB reads 142 ns -> 98.3 ns
ingest, 1MB reads 38.4 us -> 19.2 us
dispatch 171 ns -> 99.1 ns

traceconv also drops a 64MB read buffer: peak RSS 62.7MB -> 13.6MB.

@LalitMaganti
LalitMaganti requested a review from a team as a code owner August 28, 2026 01:32
@primiano

Copy link
Copy Markdown
Member

I'm going to take these reviews if you don't mind, as I wrote these classes

@primiano
primiano self-requested a review August 28, 2026 09:17

@primiano primiano left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice. LGTM % one question on what hapepend to unixd.cc, i'm confused on how that can still work.

Comment thread src/trace_processor/rpc/unixd.cc
@LalitMaganti
LalitMaganti force-pushed the dev/lalitm/zerocopy-1-ringbuffer branch from 3b19ce9 to 08af104 Compare August 28, 2026 10:19
@LalitMaganti
LalitMaganti removed the request for review from safayat-google August 28, 2026 10:25
@LalitMaganti

Copy link
Copy Markdown
Member Author

Very nice. LGTM % one question on what hapepend to unixd.cc, i'm confused on how that can still work.

Replied, PTAL

@primiano primiano left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

much better, LG with some minor questions

Comment thread src/protozero/proto_ring_buffer.cc Outdated
Comment thread include/perfetto/ext/protozero/proto_ring_buffer.h Outdated
Comment thread src/trace_processor/rpc/unixd.cc Outdated
@LalitMaganti
LalitMaganti force-pushed the dev/lalitm/zerocopy-1-ringbuffer branch from 08af104 to 537a808 Compare August 28, 2026 15:10
@LalitMaganti

Copy link
Copy Markdown
Member Author

Reworked it as you suggested. PTAL.

@LalitMaganti
LalitMaganti force-pushed the dev/lalitm/zerocopy-1-ringbuffer branch from 537a808 to 0ab6f73 Compare August 28, 2026 15:30
Every transport kept a staging buffer, read into it, then handed those
bytes to Append(), which copied them again. BeginWrite()/EndWrite() lets
read(2) deposit them in the ring buffer directly, so they are copied
once.

The reservation is handed out as a move-only WriteHandle that must be
consumed exactly once, by EndWrite() to keep what was written or
AbortWrite() to discard it; dropping one fails a CHECK. Holding it in a
value the compiler tracks is what makes the pattern safe to use: the
buffer refuses to recompact, grow or hand out a second reservation while
one is outstanding, so bytes cannot move under a pointer someone still
holds. Rpc::RequestHandle carries the same contract out to the
transports.

Four bugs fell out of this, three of them a reservation abandoned on a
path that returns early:

- The size of the reservation used to live in a member alongside the
  pointer, and the path that gives up on a stream too long to ever form
  a message returned without setting it. The caller's next EndWrite()
  then failed its CHECK, so a peer could abort the process by sending
  128MB of varint continuation bytes. The count now lives in the handle,
  where it cannot fall out of step with the pointer it describes.
- traceconv dropped the reservation it sniffs the first chunk in
  whenever the trace turned out to be compressed, and again whenever the
  decompressor errored.
- RemoteTraceProcessor dropped one whenever a recv failed.

unixd additionally re-serialized each tokenized message back into its
TraceProcessorRpcStream framing and pushed it through a second ring
buffer inside Rpc. Rpc::OnRpcMessage() dispatches it as-is.

  out/linux_clang_release/perfetto_benchmarks \
      --benchmark_filter='ProtoRingBufferIngest|ProtoRingBufferDispatch'
  ingest, 4KB reads    142 ns -> 98.3 ns
  ingest, 1MB reads   38.4 us -> 19.2 us
  dispatch             171 ns -> 99.1 ns

traceconv also drops a 64MB read buffer: peak RSS 62.7MB -> 13.6MB.
@LalitMaganti
LalitMaganti force-pushed the dev/lalitm/zerocopy-1-ringbuffer branch from 0ab6f73 to fe1f2d8 Compare August 28, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants