10 KiB
10 KiB
09 — Cons, Gotchas, & Pain Points
A candid list of everything that will hurt if you adopt FlatBuffers for this project — and how to mitigate each.
1. No first-party Odin binding (the biggest risk)
- FlatBuffers officially supports many languages; Odin is not one. The Odin side requires either (a) C ABI FFI to the third-party FlatCC runtime, (b) a hand-rolled pure-Odin reader (100-300 lines, plus maintenance forever), or (c) reuse of OdinArrow's hand-rolled FlatBuffers decoder.
- Impact: schema changes ripple into your Odin code, not generated code. Update discipline + the conformance suite are mandatory, not optional.
- Mitigation: commit static
.binfixtures and hard-code the conformance tests; keep the schema churn low; consider a tiny code generator if the schema grows fast.
2. Serialization is harder than JSON/protobuf (back-to-front building)
- Objects are built children-before-parents: create all strings/vectors/ nested tables first, then the parent that references them. FlatCC's builder is oriented differently (start/end + stack) and also non-obvious.
- Error-prone ordering bugs are caught only by round-trip tests.
- Impact: server-side code is more verbose; onboarding cost.
- Mitigation: wrap builder logic in small helper functions per message type;
comprehensive round-trip tests; the columnar layout keeps touching-code
minimal (just
create_vector× columns).
3. Larger on the wire than protobuf
- vtables add 4-16 bytes per table; scalar vectors are NOT varint-packed (every f64 is 8 bytes regardless). FlatBuffers is ~20-50% larger than protobuf on comparable payloads; in the Go benchmark, proto 82B vs FB 192B on a small object.
- Impact: for millions of objects over a constrained link this is real bandwidth. For a LAN/k3s homelab renderer, negligible.
- Mitigation: columnar vectors eliminate per-row vtable overhead; omit default-valued fields (free savings); compress with gzip/zstd at the transport (WebSocket payloads are compressible — but you lose zero-copy if you decompress into another buffer; keep compressed frames bounded).
4. Not self-describing / schema coupling
- A FlatBuffer is meaningless without its
.fbs. Clients (and your future self debugging withcurl) cannot inspect a buffer without the schema. - Schema must be shared and version-disciplined between api/ and gui/ —
exactly the drift problem that
data.odin's JSON mirrors already suffer. - Impact: debugging is harder; schema changes are coordinated releases.
- Mitigation: file identifiers (
file_identifier "DESI") for self-checking; generate JSON from buffers (flatc --json --schema foo.fbs -- foo.bin) for debugging/inspection; keep aschema/in-repo single source of truth;flatc --conformin CI.
5. Immutable buffers / no in-place mutation
- Once built, you cannot edit a FlatBuffer in place (the old C++ JSON path had reflective mutation; Rust's reflection/resizing is experimental). "Fix a field" → rebuild the whole buffer.
- For the renderer this is fine (it's read-only). For the server it means any per-client customization (filtering, projections) happens before build, or you build per-client variants (expensive).
- Mitigation: build canonical buffers; push client-side filtering to the client (it's zero-copy cheap for the client to skip rows).
6. 2 GiB buffer ceiling
- Single FlatBuffer cannot exceed 2 GiB (
u32offsets). A full DESI survey is millions of rows × ~24-40 bytes = gigabytes. You must batch. - Impact: streaming design becomes mandatory (fine — it's the plan).
- Mitigation: batches of ≤ 1-4 MB;
batch_seqto reassemble/order; client holds N buffers, not one gigantic one.
7. Verification is required for untrusted input
- Without verification, field reads on a malformed buffer → OOB (Rust: panic /
UB with
_unchecked; Odin: guaranteed memory unsafety). Your data comes from a network — verify. - Rust's verifier is good but "experimental" historically: configurable
VerifierOptions(max_depth/max_tables) help against DoS via deep buffers. - Impact: an extra O(n) pass per frame. For a 4 MB batch this is sub-ms — acceptable; still, do it once per batch, not per accessor.
- Mitigation: verify on ingest for untrusted streams; for trusted replay
(local fixtures) you may use
_uncheckedfor the benchmark path; addmax_*caps.
8. Cross-build/toolchain friction
- flatc/flatcc development video: version pinning matters — generated code must
match the runtime crate version. Mismatched
flatc↔flatbufferscrate → subtle incompatibilities. - The repo pins Odin version and Rust version; now you pin flatbuffers too (schema → generator → runtime all consistent), plus (Path A) compile flatcc under Emscripten for WASM.
- Impact: build-system surface area grows.
- Mitigation: commit generated code (skip codegen in CI); pin exact crate/
tool versions in
api/Cargo.toml, gui Makefile, and CI YAML; centralize flatc install (scripts/ likeinstall_odin.shpattern).
9. WASM/Emscripten path is the fuzziest area
- Zero-copy across the JS↔WASM boundary doesn't exist; JS decodes FlatBuffers (official lib) then hands typed arrays into WASM heap (one memcpy) — you've given up the headline feature for the web build.
- Or the Odin WASM code reads frames delivered by JS; but then JS→WASM handoff
doubles as a copy anyway (unless you use
WebAssembly.Memoryshared views and careful ownership). - Impact: web build behaves worse than native (memcpy + JS decode), which
is expected but worth stating. Native desktop (
make run) is unaffected. - Mitigation: accept one memcpy for web; keep the native zero-copy path clean; test both.
10. Odin-type ergonomics: optional scalars, enums, slices
- Odin has no
Option<T>/Resultsugar. Optional scalars (default: null) surface as "present? + value" pairs you hand-roll: anis_set boolflag (or NaN sentinel for f64) alongsidevalue: f64. - Enums are fine (Odin
enumwith explicit values) but vector-of-enum reads as[]bytewith manual cast. - Reading column vectors →
[^]f64/[]f64requires the buffer outlive the view: zero-copy slicing borrows the frame buffer. Freeing the frame before the render pass = dangling[]f64. Ownership discipline (defer-free) needed. - Mitigation: document the "frame buffer must outlive render pass" rule; write copy-free helper procs that take the buffer explicitly; test with over-release patterns.
11. Debugging binary data
- No
curl | jq. Binary payloads are opaque without tooling:flatc --json(official) and flatcc's JSON printer (per-schema C) are your tools. - IDE display of buffers is limited; hex dumps are the fallback.
- Impact: slower onboarding for new contributors; harder support/debug sessions.
- Mitigation: keep the JSON endpoints (health/catalogs) for humans; add a
tiny dev CLI (or make target) that dumps a
.binto JSON via flatc; log length/hash per frame instead of contents.
12. Ecosystem / library maturity wrinkles
- Rust flatbuffers crate is labeled "experimental" in docs although widely used; API churn across minor versions historically (see docs.rs). Pin precisely.
flatbuffers-builduses a symlink-in-src hack to surface generated code to build.rs — a smell that can break with Cargo changes. Committing generated code avoids it (recommended here).- No official Odin, no official C# for web-unity-esque paths, no protobuf-style RFC coverage. Small but real gaps.
13. Opinionated schema discipline
- Teams that skip evolution rules (reordering, deleting, default changes) get silent data corruption — worse than an explicit break because reads don't error, they return wrong values.
- Rule: new fields only at table end; deprecate (don't delete); don't change
defaults; enums only grow; structs are frozen forever. Enforce with
flatc --conform+ review discipline.
14. Odin ecosystem: borrowing from OdinArrow is a dependency decision
- OdinArrow is community-maintained (TimeLord). If you extract only its
FlatBuffers decoder, you own a fork; if you depend on it wholesale, you hold
a third-party Odin dependency in
gui/lib/with its own update cadence. - License + maintenance verification required before adoption (the repo's
add-depflow exists for exactly this: submodule intogui/lib/local/).
15. Performance expectation management
- Benchmarks are message-shape-dependent. FlatBuffers' deserialization reads are near-zero, but serialization is not fastest (Mechanical Snail: FB serialize 0.9µs vs Cap'n 0.5µs). The distributed-latency study even found protobuf/avro/thrift beat FlatBuffers on end-to-end latency in some microservice topologies because payload size dominates network time.
- For LAN renderer workloads, FlatBuffers' win is real (read-side) but don't over-promise on shrink: it's bigger than protobuf.
- Measure with your own criterion bench before/after (see
07-alternatives.mdsuggestion).
Pain-point severity ranking
| # | Pain | Severity | Mitigation exists? |
|---|---|---|---|
| 1 | No Odin binding (FFI/hand-roll) | High | Yes (flatcc / OdinArrow / conformance) |
| 2 | Back-to-front serialization complexity | Medium | Yes (helpers + tests) |
| 3 | Bigger wire than proto | Low-Medium | Yes (columnar + omit defaults) |
| 4 | Not self-describing | Medium | Yes (fixture files + file id) |
| 5 | Immutability | Low | Yes (rebuild per batch) |
| 6 | 2GiB ceiling → batching | Low | Yes (batch streaming) |
| 7 | Verifier needed for untrusted input | Medium | Yes (verify once per batch) |
| 8 | Toolchain/version pinning | Medium | Yes (commit generated, pin versions) |
| 9 | WASM zero-copy loss | Low (web is secondary) | Yes (accept 1 memcpy) |
| 10 | Odin ergonomics (Option/slices/borrow) | Medium | Yes (patterns + tests) |
| 11 | Debugging binary | Low | Yes (flatc --json + keep JSON endpoints) |
| 12 | Library maturity wrinkles | Low | Yes (pin, commit generated) |
| 13 | Schema discipline | Medium | Yes (flatc --conform in CI) |
| 14 | OdinArrow dependency | Low | Yes (license/maintenance review) |
| 15 | Performance expectation | Low | Yes (own benchmarks) |
Overall: all pain points have identified mitigations; the two that require active, ongoing investment are (1) the Odin reader and (13) schema discipline. Budget for them in the implementation plan.