zstd level barely changes decode speed; block size is what hurts random-access latency in a chunked time-series viewer
Setting: lossless per-signal blocks of value changes for a waveform/time-series viewer. Pipeline: varint time deltas, XOR/dictionary value coding, then zstd per block, plus a level-of-detail pyramid for zoomed-out views.
Measured on an Apple M-series laptop on AC power, native C++, the same 64 signals re-encoded: - zstd-3, 16K-change blocks: 0.253 B/change; encode 0.10 us/change, decode 4.6 ns/change. - zstd-19, 16K blocks: 0.172 B/change (32% smaller); encode 1.37 us/change (13x slower), decode 5.6 ns/change. - zstd-19, 64K blocks: 0.139 B/change; decode 4.4 ns/change.
Takeaways: 1. A higher zstd level is close to free for readers. Pay for it at conversion time: write fast at level 3 so data is viewable immediately, then recompress in the background at level 19 and swap the blocks in. 2. Bigger blocks compress 10-15% better, but a cold random-access frame has to decode the whole block containing the requested time for every visible signal. 4x bigger blocks means about 4x the cold-frame work. With 100 heavy signals on 8 decode threads, 16K blocks gave a cold p95 of about 4 ms; 64K blocks would take an estimated 10-17 ms, at or over a 16.7 ms frame budget. Warm pans and zoomed-out LOD views aren't affected. 3. Single-threaded cold decode of heavy signals was 14-25 ms per frame. Parallel block decode (prefetch, then render) was needed to get under budget. 4. The bigger win than either setting was storing identical streams once: port copies of the same net were about 45% of the bytes.
Suggested policy: small blocks for anything a user views, big blocks only for an archive tier that is re-split into small blocks the first time it's opened.