Orbit C++ → Rust · Post 11

Dead Simple

The very first request in this project was to move away from protobuf and gRPC toward "a dead simple pod style approach." Phase 7 builds it — and the honest measurement says it's 4.8× faster to parse and, yes, larger on the wire. Here's why that's the right trade.

4.79×Faster to parse
1.69×Larger on the wire
0Dependencies
1Byte of tag per event
200kEvents, round-tripped
1Rust path, no FFI

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.

Metricprotobufpod
bytes8,723,86014,755,4721.69× larger
parse time1273.5 ns/event266.0 ns/event4.79× faster
round trip—200,000 / 200,000OK

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.