8.3 KiB
8.3 KiB
07 — Alternatives to FlatBuffers
FlatBuffers isn't the only option. This doc compares the realistic alternatives for a Rust API → Odin GUI streaming pipeline. The evaluation lens: cross- language support (Rust + Odin), zero-copy reads, schema evolution, streaming fitness, and ecosystem maturity.
Quick comparison table
| Format | Zero-copy reads | Schema | Wire size (vs JSON) | Serialize | Deser/read | Streaming | Rust support | Odin support |
|---|---|---|---|---|---|---|---|---|
| JSON (current) | No | No | 1.00× (268B) | 5.2 µs | 8.7 µs | SSE/chunk | serde | core:json |
| MessagePack | No | No | 0.60× | 2.1 µs | 3.8 µs | chunk | rmp-serde | tgolsson/msgpack-odin (small) |
| Protocol Buffers | No | Yes (proto) | 0.35× | 1.8 µs | 2.4 µs | gRPC streaming | prost/tonic | none official (C via protobuf-c) |
| FlatBuffers | Yes | Yes (fbs) | 0.47× | 0.9 µs | 0.03 µs | size-prefix/WS | flatbuffers crate | none official (C via flatcc) |
| Cap'n Proto | Yes | Yes | 0.54× | 0.5 µs | 0.02 µs | structured RPC | capnp crate | none official (C via capnproto-c) |
| Apache Arrow IPC | File/stream batch | Yes (FlatBuffers-based) | ~0.42× (typed cols) | batch-append | ~zero-cost column slices | native stream format | arrow crate (official) | OdinArrow (community) |
(Benchmarks: Mechanical Snail wire-format study, Nov 2025, Rust, 1-MB
median message; 0.47× etc. are compression ratios; absolute numbers vary by
message shape — see 10-performance-benchmarks.md.)
Deeper per-format notes
JSON (status quo)
- Pros: human-readable, debug-friendly, universal, no schema burden, trivially works with curl, streaming via SSE/chunked HTTP.
- Cons: slowest (5-14× slower than binary on read-heavy paths), verbose (2-3× payload size), no type integrity, allocates per-field on parse.
- In this repo: already stubbed. Could stay for the HTTP diagnostics path, but for streaming 100k+ points per frame to a renderer, JSON is the wrong tool.
- Verdict: keep for curl/debug endpoints; not the streaming wire.
MessagePack
- Pros: binary offshoot of JSON — schema-less, faster, 40% smaller. Rust (rmp-serde) is mature. There's a small pure-Odin msgpack impl (tgolsson/ msgpack-odin — timestamps included, no ext types).
- Cons: no schema → no forward/backward compatibility guarantees, no zero-copy reads (you parse into Odin structs per field), no type end-to-end integrity. On reads it's only ~2× faster than JSON — the renderer still pays a per-field decode.
- Verdict: good "fast JSON" but doesn't solve the zero-copy/schema-evolution problems.
Protocol Buffers (protobuf)
- Pros: smallest wire (varint encoding); gRPC for streaming; smallest payloads; excellent Rust (prost/tonic, prost-build); schema-evolution via field numbers; huge ecosystem.
- Cons: no zero-copy reads — deserialization allocates the full object graph. For 100k objects/frame that's the exact cost FlatBuffers eliminates. No first-party Odin; C via protobuf-c is clunky.
- Verdict: ideal for "fat service-to-service RPC" where you decode once and hold objects. Wrong shape for a per-frame high-frequency render workload unless you pre-decode into the renderer's own buffer.
Cap'n Proto
- Pros: true zero-copy (wire layout ≈ memory layout), pointer-based lazy access, built-in RPC, slightly simpler encode than FlatBuffers (no backward construction). Serialize ~0.5 µs, deserialize ~0.2 µs (fastest table entries in the Mechanical Snail study).
- Cons: 8-byte alignment makes it ~20-50% larger than FlatBuffers on the wire; no Robert-native Odin; lower ecosystem/maturity than FlatBuffers; fewer language bindings (no official WASM/JS story as rich).
- Verdict: a genuinely strong competitor, but for this repo the wiresize penalty (bandwidth for millions of points) and weaker Odin/JS ecosystem tilt toward FlatBuffers.
Apache Arrow IPC (columnar, used as transport)
- Pros: the canonical columnar data format; the only alternative with a
native Odin implementation (TimeLord/OdinArrow) already shipping a
Zero-copy IPC reader (memory-mapped, pyarrow-interopable). Streaming file/
stream formats are purpose-built for bulk numeric data. Rust side gets the
official
arrowcrate. - Cons: bigger dependency surface (arrow crate is heavy); schema model is
Arrow's own (Field/RecordBatch) rather than your own; IPC header uses
FlatBuffers internally (Arrow metadata is FlatBuffers), which is a point in
FlatBuffers' favor; existing
CatalogObjectmodels don't map 1:1. - Verdict: strongest technical fit for "millions of numeric rows, columnar, zero-copy, streaming". If the DESI dataset is really millions-of-points columnar, Arrow IPC deserves a serious look — but you'd be adopting Arrow's framing + OdinArrow dependency rather than your own schema/format, which reads as more risk for a repo this young.
Flexbuffers / schemaless (bonus)
- FlatBuffers ships Flexbuffers — its own schemaless, self-describing, variable-typed format (kind of a fast MessagePack). Rust+JS+Python etc. supported; Odin via same C ABI story as FlatBuffers.
- Not a fit here: the point of going FlatBuffers is the shared compiled schema keeping Rust/Odin coherent. Flexbuffers gives up exactly that.
Decision matrix mapped to DESI Explorer
| Requirement | FlatBuffers | Cap'n Proto | Protobuf | Arrow | MsgPack |
|---|---|---|---|---|---|
| Zero-copy per-frame reads (100k+ pts) | ✅✅ | ✅✅ | ❌ | ✅✅ | ❌ |
| Compact wire for many f64 columns | ✅ (f64 vectors) | ⚠️ (8B-aligned, larger) | ✅ (varint packed) | ✅✅ (chunked) | ⚠️ |
| Schema evolution (edr→dr1→dr2) | ✅ | ✅ | ✅✅ | ⚠️ (Arrow schema is rigid) | ❌ |
| Rust (API) maturity | ✅ (official crate) | ✅ (capnp crate) | ✅✅ (prost/tonic) | ✅✅ (official) | ✅ |
| Odin (GUI) availability | ⚠️ C-ABI/flatcc or hand-rolled | ⚠️ C-ABI or hand-rolled | ❌ (protobuf-c barely) | ✅ (OdinArrow) | ⚠️ (small impl) |
| WASM/JS story | ✅ (official npm) | ⚠️ | ✅ | ⚠️ | ✅ |
| Streaming primitives | ⚠️ (BYO framing; flatstream-rs exists) | ✅ (RPC) | ✅ (gRPC) | ✅✅ (IPC stream) | ⚠️ (chunking only) |
Bottom line
- FlatBuffers is the best default for this project: the zero-copy read model matches the render loop, the wire format is compact for numeric vectors, schema evolution fits DESI's release cadence, and the Rust + WASM/JS official story is strong. The Odin gap is real but solvable (C-ABI via flatcc, hand-rolled reader, or OdinArrow patterns).
- Apache Arrow IPC is the strongest alternative if the data is truly "columnar millions-of-rows" and you accept OdinArrow + the Arrow header/ framing dependency. It even uses FlatBuffers internally for its metadata, so it's not an either/or at the format level.
- Protobuf/gRPC wins only if you'd rather have official Rust streaming + smallest payloads and are OK decoding into objects once (not per frame) — the renderer then builds its own draw buffers. For 100k objects/frame the "decode-once-into-draw-buffer" model is actually defensible; measure before over-engineering.
Test-before-commit suggestion
Run a micro-benchmark (criterion) on your actual payload shape (e.g. 10k objects × 3 f64 columns / batch) comparing:
- serde JSON → parse
- FlatBuffers columnar batch (this proposal)
- protobuf (prost) → decode to Vec
- Arrow IPC record batch (arrow crate) → record batch slices
Numbers will decide the finer trade-offs; the qualitative table above is the strategic direction.
References
- Mechanical Snail, "Wire Formats for High-Volume Service Communication" (2025-11): https://mechanicalsnail.com/posts/wire-formats-rust/
- Youngju, "Binary Serialization Complete Guide" (2025-2026): https://www.youngju.dev/blog/...-binary-serialization-...
- Maltsev & others, "Impact of Serialization Format on Inter-Service Latency" (2024): https://science.lpnu.ua/sites/default/files/journal-paper/2024/dec/...
- Opentraffic/DEEPWIKI Flatbuffers deep dive: https://deepwiki.com/opentraffic/flatbuffers
- Arrow IPC / OdinArrow: https://arrow.apache.org/docs/format/Columnar.html + https://github.com/TimeLord/OdinArrow
- msgpack-odin: https://github.com/tgolsson/msgpack-odin