Skip to content

tracing: add append-only proto encoding for v2 writers - #7325

Draft
sashwinbalaji wants to merge 2 commits into
dev/sashwinbalaji/tracing-v2-step1-ring-buffer-stackfrom
dev/sashwinbalaji/tracing-v2-step1-protozero-stack
Draft

tracing: add append-only proto encoding for v2 writers#7325
sashwinbalaji wants to merge 2 commits into
dev/sashwinbalaji/tracing-v2-step1-ring-buffer-stackfrom
dev/sashwinbalaji/tracing-v2-step1-protozero-stack

Conversation

@sashwinbalaji

@sashwinbalaji sashwinbalaji commented Sep 3, 2026

Copy link
Copy Markdown
Member

Protozero normally back-patches nested-message lengths. The v2 ring can expose committed prefixes before their enclosing messages close, so packets written there need append-only framing.

  • Add opt-in ProtoGroup framing to the C and C++ Protozero APIs while keeping length-delimited protobuf as the default.

  • Use one private C++ framing discriminator for length-delimited messages, ProtoGroup roots and nested ProtoGroup messages without growing Message.

  • Preserve the public C message layout and reject incremental STRING and PACKED fields in ProtoGroup mode until buffered construction is needed.

  • Carry the selected encoding through C data-source packet creation while keeping the legacy entry point restricted to length-delimited writers.

  • Pin the C++ Message size to its previous 32-bit and 64-bit debug and release budgets.

  • Rewrite copied ProtoGroup packets into ordinary length-delimited protobuf, validating the wire format and bounding output and nesting links.

  • Implement TraceWriterV2 fragmentation, continuation, loss reporting and append-only finalization on SharedRingBufferWriter.

  • Make the retained TraceWriterV2 delegate provide the ring so its storage and coordination share one lifetime contract.

  • Test exact C and C++ encodings, malformed input, output limits, chunk-boundary finalization, scraping, drops, direct stream reservations, delegate retention and unchanged defaults.

@sashwinbalaji
sashwinbalaji force-pushed the dev/sashwinbalaji/tracing-v2-step1-protozero-stack branch from 673a5c4 to 1d1afad Compare September 4, 2026 09:14
@sashwinbalaji
sashwinbalaji force-pushed the dev/sashwinbalaji/tracing-v2-step1-protozero-stack branch 2 times, most recently from 2f34eb2 to 455aac3 Compare September 8, 2026 07:16
@sashwinbalaji
sashwinbalaji force-pushed the dev/sashwinbalaji/tracing-v2-step1-protozero-stack branch from 455aac3 to 3a756b6 Compare September 8, 2026 10:05
@sashwinbalaji
sashwinbalaji force-pushed the dev/sashwinbalaji/tracing-v2-step1-protozero-stack branch from 3a756b6 to 6bc6694 Compare September 8, 2026 12:41
@sashwinbalaji
sashwinbalaji force-pushed the dev/sashwinbalaji/tracing-v2-step1-protozero-stack branch from 6bc6694 to 287be4c Compare September 9, 2026 14:58
Protozero normally back-patches nested-message lengths. The v2 ring can expose committed prefixes before their enclosing messages close, so packets written there need append-only framing.

- Add opt-in ProtoGroup framing to the C and C++ Protozero APIs while keeping length-delimited protobuf as the default.

- Use one private C++ framing discriminator for length-delimited messages, ProtoGroup roots and nested ProtoGroup messages without growing Message.

- Preserve the public C message layout and reject incremental STRING and PACKED fields in ProtoGroup mode until buffered construction is needed.

- Carry the selected encoding through C data-source packet creation while keeping the legacy entry point restricted to length-delimited writers.

- Pin the C++ Message size to its previous 32-bit and 64-bit debug and release budgets.

- Rewrite copied ProtoGroup packets into ordinary length-delimited protobuf, validating the wire format and bounding output and nesting links.

- Implement TraceWriterV2 fragmentation, continuation, loss reporting and append-only finalization on SharedRingBufferWriter.

- Make the retained TraceWriterV2 delegate provide the ring so its storage and coordination share one lifetime contract.

- Test exact C and C++ encodings, malformed input, output limits, chunk-boundary finalization, scraping, drops, direct stream reservations, delegate retention and unchanged defaults.
@sashwinbalaji
sashwinbalaji force-pushed the dev/sashwinbalaji/tracing-v2-step1-protozero-stack branch from 287be4c to f015a42 Compare September 10, 2026 00:24
@primiano
primiano self-requested a review September 10, 2026 09:15

@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.

I have some limited time today but i'm sending some intiial comments.
I only looked at message.h/cc, trace_writer_v2.h/cc, which I believe are the more intetesting ones and worth getting comments sooner.

I didn't look at the rewriter, but givenm teh signature I suppose it's non controversial, to the point where I'd blindly trust a decent AI Agent to deal with that.

I dind't look at the shared_lib stuff because i'm not fmailiar with it and woudl take me too time today to look. but again I don't think there is a lot of juicy stuff there

//
// In proto-group mode, a nested message ends with a closing byte and the
// root ends at the packet boundary. Neither writes a length before its
// contents, so there is no length field to fill in later.

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.

I'm assuming that in group mode the size_field is always null?
In this case make the comemnt stronger. Say "this is unused and always null in group mode".
Or maybe it's set but ignored (which is also fine). but then say which one is which.

EDIT after reading the message.cc looks like it's always null.

// END closes the child. No additional byte closes the root.
// Both messages use the same encoding, so we need is_root_ to tell them
// apart.
bool is_root_;

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.

I'm going to be a bit handwavy here, but I wonder if we can leverage the fact that we have already the notion of a RootMessage (for arena purposes).
TraceWritrer (and also your V2) has a reference to a root message.
So I am thinking something on the lines of:

  • We make Message::Finalize take an optional argument, so Finalize(bool is_root=false)
  • We override (without making it virtual) the Finalize method in RootMessage (keeping it a void)
    so something like:
class RootMessage : public T {
  ...
  Finalize() {
    Finalize(/*is_root=*/true);
  }
}

I think this shoudl have the same effect without adding further state tracking.
But I have not tought deeply to this, so don't take it as granted.
I wouldn't be surprised if you found some good reason why we have to do your way.

Comment thread src/protozero/message.cc

// Write the proto preamble for the nested message.
uint32_t tag;
switch (encoding_) {

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.

Imho the switch(encoding_) is 1. overkill, 2. inconsistent with what you did above (where you had an if(UNLIKELY)). Pick one and be consistent.
I'd honestly just go with a plain if.

siwtch adds more nesting, and sometimes causes problems with older compilers about "what if it's option 3".

// Wire types 3 and 4 delimit a protobuf group. Protozero decoders do not
// support groups, so keep them out of ProtoWireType. Tracing v2 only borrows
// the start-group wire type for the encoding below.
constexpr uint32_t kWireTypeStartGroup = 3;

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.

When commenting on kWireTypeStartGroup explain that we bastardized group (okay not in those terms) and don't follow the official encoding for it (you can link at the RFC)

// As with TraceWriterImpl, an instance may be used by only one thread at a
// time. Packet writes go straight to the ring; the delegate handles reader
// notifications, flush completion and WriterID retirement.
class TraceWriterV2 : public TraceWriter,

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.

Maybe purely for naming consistency (when comparing and doing codesearch) this should be called TraceWriterV2Impl, as thi feeels the moral equivalent of TraceWriterImpl, right?


struct InitArgs {
// Must be non-null. Retained until the writer is destroyed.
std::shared_ptr<Delegate> delegate;

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.

out of curiosity why the Deletgate is a shared_ptr and not a unique_ptr?
A TraceWriter is not thread-safe (it's supposed to be used on the same thread, or caller must use a lock), so I'm not sure on why you need the refcounting here?


// Called after the writer publishes its last chunk. The WriterID must not
// be reused while unconsumed ring positions can still name it.
virtual void OnWriterDestroyed(WriterID) = 0;

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.

uh why the delegate has this notirication? isn't the owner of the delegate supposed to know when the writer is destoryed?

I think I don't fully understand how ownership/lifecycle works for TWv2

cur_packet_(new protozero::RootMessage<protos::pbzero::TracePacket>()) {
// Protozero never receives more than one chunk at a time. Reserving the same
// amount here keeps the drop path allocation-free.
drop_buffer_.resize(delegate_->ring_buffer().chunk_size());

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.

can we lazily allocate this if we ever need it, rather than pre-serving one chunk per thread? feels too aggressive memory-wise.
Or actually can the drop buffer be one for all (i.e. a global in the anonymous name space here) rather than one per writer?
is it like the garbage chunk? or do we care about ownerhsip of the writes ? is this going to do CAS & co on this drop_buffer?

// Publish the last chunk before notifying the delegate of destruction.
ring_buffer_writer_.FinishCurrentChunk();
stream_writer_.Reset({nullptr, nullptr});
delegate_->OnWriterDestroyed(writer_id());

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.

isn't the delegate the same obhect that owns TraceWriterV2 (which means it could tell precisely when it's going to be destroyed?)

stream_writer_.Reset(EnterDropMode());
}

// Every nested message inherits the root's encoding.

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.

i'm not sure I understood this comment

@sashwinbalaji

Copy link
Copy Markdown
Member Author

I dind't look at the shared_lib stuff because i'm not fmailiar with it and woudl take me too time today to look. but again I don't think there is a lot of juicy stuff there

There is a slilghtly controversial thing in include/perfetto/public/pb_msg.h , I would like your opinion on. About how I am (ab)using PerfettoPbMsg::size field

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