redis.packed codec: ~63% of a decompress is threadpool dispatch, not compression — make sync/async size-conditional
Summary
redis.packed's brotli codec runs every value through util.promisify(zlib.brotli*), i.e. one libuv threadpool round trip per value. For the small values the compressed caches actually hold, the dispatch costs more than the compression does.
Measured on real cached values (n=48, compressed p50 883 B / uncompressed p50 1,671 B), timed through the same promisified shape packed-compression.ts uses:
| p50 | p90 | |
|---|---|---|
| dispatch floor (1-byte payload — the round trip with ~no codec work) | 17.7 µs | 38.6 µs |
| decompress, async (what ships today) | 21.9–30.2 µs | 41.9–66.2 µs |
| decompress, sync (identical work, no dispatch) | 11.1 µs | 17.6 µs |
| compress q6, async | 111.6 µs | 216.3 µs |
~63% of every async decompress sample is threadpool dispatch, not codec work. At production read volume that is roughly 2.7× more wall time than the compression itself costs, and it is paid per key — packed.mGet is Promise.all(keys.map(get)), so a single cache call fans out to ~90 independent threadpool round trips.
Why not simply make it sync
The async choice is deliberate and packed-compression.ts documents why: a worst-case tensor-metadata value is ~335 KB and measures ~36 ms compress / ~5 ms decompress. Running that synchronously would block the event loop, which is exactly what the current design avoids. That reasoning is correct and should not be reverted.
Proposal
Pick sync or async by payload size, not globally:
- below a threshold (~64 KB is a safe starting point; the real caches sit at ~1–4 KB), use
brotliDecompressSync/brotliCompressSync— at 11 µs p50 / 17.6 µs p90 this is orders of magnitude below any long-task threshold and removes ~19 µs of dispatch per call; - at or above it, keep the promisified path exactly as it is today, so the large-value protection is untouched.
Both branches produce identical bytes, so this is not a format change and needs no sentinel work or key-bust.
Worth checking while in here
The threshold should be chosen against a measured distribution rather than assumed — the two caches that opt into compress today have very different value-size profiles, and only one of them motivated the async design.
Closing condition
Closed when either a merged PR makes the codec path size-conditional and a follow-up measurement shows the decompress p50 at or below the sync figure above, or a written decision in this issue declines it with the reason. Checked by whoever next edits packages/civitai-redis/src/packed-compression.ts.
Context
Found while measuring the cost of #4649 (brotli on the image-meta cache) for #4654 (the codec duration histogram). Neither PR is blocked on this, and it should not be folded into either — #4654 is an observability change and this is a hot-path optimisation.
Source: civitai/civitai