Binary/compact on-disk trace format — JSON does not scale to multi-million-point series #307
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Surfaced during spec review of the tap-subscribers cycle (refs #283, #77).
Problem
The trace store persists every tap as a
ColumnarTraceJSON file (crates/aura-registry/src/trace_store.rs; shape{tap, kinds, ts: Vec<i64>, columns: Vec<Vec<f64>>}). For the full-history study this cycle is built for (~4.5 M M1 cycles), one tap file becomes ~60–120 MB of number text:read/read_familydoread_to_string+serde_json::from_str— the whole file as aStringplus the parsed vectors in memory, per tap, per chart request.Direction (not prescribing)
A compact columnar format for the two hot arrays (
ts,columns) — e.g. length-prefixed little-endian i64/f64 blocks, JSON retained only for the smallindex.json/manifest metadata. Decision surface: the read side (read,read_family, the chart serve/decimation path), forward/backward compatibility or a one-shot migration, and whetherkindsstays a JSON sidecar.Context
The #283 tap-subscribers cycle deliberately made the opposite call for its own scope: no format migration, streamed writer pinned byte-equal to the legacy JSON writer, every reader untouched. That keeps the cycle small; this issue is the tracked follow-up lever for when trace size actually hurts. The streamed-writer seam (
TapTraceWriter) landing in that cycle is the natural place a binary encoder would plug into.context: filed on user decision during spec review, 2026-07-21.
Additional requirements from #320 (generalizing the trace store into a recorded-stream store that holds engine inputs as well as taps), recorded here so the encoding chosen for this issue does not force a second migration if #320 lands:
TraceStore::read/read_familycurrentlyread_to_string+ parse each whole tap file (crates/aura-registry/src/trace_store.rs:120-143); at input scale (multi-year M1/tick series) that reproduces the eager-materialization wall theSourceseam (#71) exists to avoid. The format needs chunked storage with random access by row index and timestamp window (bounds+ windowed pull), so a lazySourceimpl can serve one chunk at a time.ColumnarTraceis already N-column (kinds/columnsare parallel arrays over onetsaxis,crates/aura-engine/src/report.rs:201-206) and the chart reader handles width > 1, but the incrementalTapTraceWriteris hard-wired to exactly one column (kind: ScalarKindsingular,crates/aura-registry/src/trace_store.rs:377-386;finish()emits a single nested"columns":[[…]]list). An OHLCV recording needs N columns sharing onetsaxis through the streamed path.f64(cell_to_f64,crates/aura-registry/src/trace_store.rs:356-369), the base type surviving only as a string tag inkinds. A binary format can storei64/bool/timestampcolumns natively —f64holds integers exactly only up to 2^53, a bound the format should not have to lean on.These requirements originate in #320; an encoding shipped tap-scoped without them forces a second on-disk migration if #320 lands. Whether they bind immediately or #307 ships tap-scoped first is part of #320's crate/namespace decision.