Protobuf is a good format. It is also a schema compiler, a code generator, a runtime library, and a varint state machine on every field of every event. On a capture hot path emitting millions of events a second, the question is not whether protobuf is good — it's whether the collector should pay for all of that per event. Phase 7 answers with the format the project asked for at the very start: a tag byte and fixed fields, nothing else.
7a — the format
Every event is one byte of tag followed by its fields in a fixed little-endian layout. No field numbers, no wire-type nibbles, no varints; variable parts — a callstack's program counters, a function call's registers, an interned string — are length-prefixed. A capture is the concatenation of these records, and a reader walks it start to end. The crate has zero dependencies: no protobuf runtime, no generated code, no build-time schema step. Emitting an event is a handful of extend_from_slices; parsing one is a handful of from_le_bytes.
It is round-trip tested for every event type — empty arrays included — and, importantly, truncation at every byte offset is an error rather than a panic, and an unknown tag is an error. A wire parser that can be handed a short buffer must fail cleanly, and this one does.
The measurement, including the part that doesn't flatter it
The differential builds 200,000 representative events both ways — as length-delimited protobuf ProducerCaptureEvents and via the pod encoder with identical field values — and measures both size and parse speed.
| Metric | protobuf | pod | |
|---|---|---|---|
| bytes | 8,723,860 | 14,755,472 | 1.69× larger |
| parse time | 1273.5 ns/event | 266.0 ns/event | 4.79× faster |
| round trip | — | 200,000 / 200,000 | OK |
The pod format is larger. Protobuf's varints pack a thread id of two thousand into two bytes and a core number into one; the fixed layout spends four or eight. Pretending otherwise would be exactly the kind of unbacked claim this log exists to avoid. What the pod format buys instead is a parse that is nearly five times faster and a codebase with no serialization dependency at all. On the hot path the bottleneck is decode CPU as events pour in, not the bandwidth of a local socket or a file that compresses well besides — so trading bytes for cycles is the right direction there. If bandwidth ever dominates, varint-encoding just the small integer fields would recover most of the size while keeping the format simple; that's a measured future option, not a default.
There is a second dividend, the one post 07 pointed at when it priced the per-event FFI toll. The collector is already FFI-free (post 08); with this format as its output, events leave the Rust world once, in bulk, as pod bytes — never one FFI call per event. The wire-format boundary that was named as the toll's cure is now built.
7b — the pieces compose
A format is a promise; a pipeline is the proof. The scheduling slice is the first place the port's parts meet as one FFI-free path: raw sched-switch records go into the ContextSwitchManager ported back in Phase 3, which pairs switch-in against switch-out into scheduling slices, which are encoded as pod SchedulingSlice events — the collector's ordering, the tracing-state logic, and the pod transport, all Rust, no boundary crossed. A run of context switches is driven through it and the emitted bytes decode back to exactly the expected slices; a timestamp regression is counted as a death, the same case the C++ ORBIT_CHECK guards, rather than producing a bogus slice.
It is one event type of many, and the sample-to-callstack path still needs its interning bookkeeping and the live end-to-end run still needs the root-gated sched tracepoints. But the shape is now visible and real: one Rust process from perf_event_open to a stream of pod bytes, with every stage on it already ported and verified. The remaining event types follow the same shape; what's left is assembly, and the service shell that drives it.