Skip to content

libutil: emit multi-frame zstd for parallel-decodable output#15550

Merged
Mic92 merged 1 commit intoNixOS:masterfrom
Mic92:zstd-multiframe-compression
Apr 13, 2026
Merged

libutil: emit multi-frame zstd for parallel-decodable output#15550
Mic92 merged 1 commit intoNixOS:masterfrom
Mic92:zstd-multiframe-compression

Conversation

@Mic92
Copy link
Copy Markdown
Member

@Mic92 Mic92 commented Mar 25, 2026

Libarchive's zstd filter produces a single frame regardless of ZSTD_c_nbWorkers, so decompression is stuck on one core. For large NARs (e.g. 11 GiB) that means ~9 seconds of single-threaded zstd while everything else waits.

Replace the libarchive zstd path with a direct-libzstd sink that cuts a new frame every 16 MiB of uncompressed input. Each frame buffers its input and compresses in one shot with an exact ZSTD_CCtx_setPledgedSrcSize, so Frame_Content_Size is written to every frame header.

Frame concatenation is mandatory in RFC 8878 §3.1, so existing nix binaries, libarchive, and the zstd CLI all decode the result unchanged. Nix currently still decompresses serially, but the independent frames with known content sizes mean a future parallel decoder can exploit them without any change to the compressed data already on disk.

When parallel=true, nbWorkers is set from
std::thread::hardware_concurrency() for MT compression within each frame; parallel=false compresses single-threaded but still emits independent frames.

The 16 MiB frame size matches zstd's 8 MiB default window well (minimal ratio loss from not referencing across boundaries) and gives ample frame counts for parallel decode of large NARs.

Motivation

Context


Add 👍 to pull requests you find important.

The Nix maintainer team uses a GitHub project board to schedule and track reviews.

@Mic92 Mic92 requested a review from edolstra as a code owner March 25, 2026 10:55
@Mic92
Copy link
Copy Markdown
Member Author

Mic92 commented Mar 25, 2026

Some performance results. Interestingly, this reduces memory usage for the producer:

 rustc 1 GiB (real-world large store path):

 ┌──────────────────────────┬───────┬──────────┬────────────┬────────┬───────┐
 │ Config                   │ Time  │ Peak RSS │ Compressed │ Frames │ Ratio │
 ├──────────────────────────┼───────┼──────────┼────────────┼────────┼───────┤
 │ Baseline, parallel=true  │ 3.77s │ 612 MiB  │ 302.6 MB   │ 1      │ —     │
 ├──────────────────────────┼───────┼──────────┼────────────┼────────┼───────┤
 │ Baseline, parallel=false │ 8.16s │ 55 MiB   │ 304.4 MB   │ 1      │ —     │
 ├──────────────────────────┼───────┼──────────┼────────────┼────────┼───────┤
 │ Patched, parallel=true   │ 5.51s │ 103 MiB  │ 302.9 MB   │ 63     │ 3.28x │
 ├──────────────────────────┼───────┼──────────┼────────────┼────────┼───────┤
 │ Patched, parallel=false  │ 7.88s │ 89 MiB   │ 305.0 MB   │ 63     │ 3.26x │
 └──────────────────────────┴───────┴──────────┴────────────┴────────┴───────┘

For consuming I couldn't measure any change.

@Mic92 Mic92 force-pushed the zstd-multiframe-compression branch from afd50c5 to b99f302 Compare March 25, 2026 13:24
Comment thread src/libutil/compression.cc Outdated
Comment thread src/libutil/compression.cc Outdated
Comment thread src/libutil-tests/compression.cc Outdated
@Mic92 Mic92 force-pushed the zstd-multiframe-compression branch from b99f302 to a9b8ff1 Compare March 25, 2026 19:59
@lovesegfault lovesegfault force-pushed the zstd-multiframe-compression branch from 20cd6c1 to 3fdc0b5 Compare March 30, 2026 16:53
@mschwaig
Copy link
Copy Markdown
Member

mschwaig commented Mar 30, 2026

Could we make parallel=true the default and maybe even remove the other option to simplify things for users on the next release (the one after landing this change)?

The default for compression could also be flipped to zstd.

@lovesegfault lovesegfault force-pushed the zstd-multiframe-compression branch from 3fdc0b5 to 5411313 Compare March 30, 2026 17:07
@github-actions github-actions Bot added the store Issues and pull requests concerning the Nix store label Mar 30, 2026
Comment thread doc/manual/rl-next/zstd-multiframe.md Outdated
Comment thread src/libstore/include/nix/store/binary-cache-store.hh
Comment thread doc/manual/rl-next/zstd-multiframe.md Outdated
Comment thread src/libutil/compression.cc Outdated
Comment thread src/libutil/compression.cc Outdated
Comment thread src/libutil/compression.cc Outdated
Comment thread doc/manual/rl-next/zstd-multiframe.md Outdated
@Mic92 Mic92 force-pushed the zstd-multiframe-compression branch from d9b5933 to 661bf44 Compare April 13, 2026 10:27
@Mic92
Copy link
Copy Markdown
Member Author

Mic92 commented Apr 13, 2026

@xokdvium this is the commit 661bf44 that addresses the code review, before I squashed everything together.

@Mic92 Mic92 force-pushed the zstd-multiframe-compression branch 2 times, most recently from 6fdcbb7 to 3ec7f1e Compare April 13, 2026 10:32
@xokdvium
Copy link
Copy Markdown
Contributor

I guess it needs some formatting.

Libarchive's zstd filter always produces a single frame, so
decompression of large NARs is stuck on one core.

Replace the libarchive zstd compression path with a direct-libzstd
sink that cuts a new frame every 16 MiB of uncompressed input, each
with an exact pledged size so Frame_Content_Size lands in the header.
Frame concatenation is mandatory in RFC 8878 §3.1, so existing nix
binaries, libarchive and the zstd CLI decode the result unchanged.
Nix still decompresses serially today, but the independent sized
frames let a future parallel decoder exploit data already on disk.

Per-frame compression uses up to 4 zstd workers (bounded by
getMaxCPU()/hardware_concurrency).  parallel-compression now defaults
to true for zstd; xz keeps its false default.  As a side effect peak
RSS during compression drops substantially (~600 -> ~100 MiB for a
1 GiB store path) with effectively unchanged ratio.
@Mic92 Mic92 force-pushed the zstd-multiframe-compression branch from 3ec7f1e to 65ff1e6 Compare April 13, 2026 10:39
@Mic92 Mic92 enabled auto-merge April 13, 2026 10:39
@Mic92
Copy link
Copy Markdown
Member Author

Mic92 commented Apr 13, 2026

Fixed the formatting.

@Mic92 Mic92 added this pull request to the merge queue Apr 13, 2026
Merged via the queue into NixOS:master with commit d92c15c Apr 13, 2026
16 checks passed
@Mic92 Mic92 deleted the zstd-multiframe-compression branch April 13, 2026 11:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation store Issues and pull requests concerning the Nix store

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants