Research-2142: Package compression and what each consumer opens¶
Question¶
Which compression can each published archive and image use so that every consumer its guide names still opens it, how much does each candidate save on the real published inputs, and what does it cost to produce?
Sources¶
Consumer versions, read 2026-10-04:
- Docker Engine 23.0.0 (2023-02-01): "Add support for pulling
zstdcompressed layers" (release notes). - Docker Desktop: 4.18.0 bundles Engine 20.10.24, 4.19.0 (2023-04-27) is the first with Engine 23.0; 4.44.0 (2025-08-07) "Fixed an issue pulling images with zstd differential layers when the containerd image store is enabled" (release notes).
- Docker manifests (v2 schema 2) with zstd layers fail to pull; OCI manifests work (docker/cli#5011).
- containerd 1.5.0 (2021-05-03): "Add support for layers compressed with zstd" (release).
- Kubernetes 1.26 requires containerd 1.6.0 or later (release blog); the chart's floor is Kubernetes 1.26 (
docs/development/k8s-deployment.md). - containers/image (Podman, CRI-O, skopeo) reads zstd since containers/image#639 (merged 2019-08-21).
- Docker Engine 20.10 reached end of life on 2023-12-10 (endoflife.date), but Debian 12 still ships
docker.io20.10.24+dfsg1-1+deb12u1 (https://sources.debian.org/api/src/docker.io/); Ubuntu 22.04 and 24.04 ship 29.1.3 in-updates(LaunchpadgetPublishedBinaries), Debian 13 ships 26.1.5. - Debian 13
Packages:gzipis Essential,xz-utilsPriority standard,zstdoptional (deb.debian.org/debian/dists/trixie/main/binary-amd64/Packages.xz). - macOS: the bundle targets macOS 14 (
MACOSX_DEPLOYMENT_TARGET=14.0inscripts/ci/build-macos-tester-bundle.sh). Apple'sdistribution-macOStagsmacos-140andmacos-150pinlibarchive-121(libarchive 3.5.3) andlibarchive-138; bothconfig.hfiles defineHAVE_LZMA_Hand leaveHAVE_LIBZSTDandHAVE_ZSTD_Hundefined, and the Xcode project linksbsdtaragainstlzma.tbd(apple-oss-distributions/libarchive). Inlibarchive-121the xz writer defaults to one thread andHAVE_LZMA_STREAM_ENCODER_MTis undefined (archive_write_add_filter_xz.c). Archive Utility opens.xz/.txz(ctrl.blog, list for 10.15.6, no.zst).bsdtar(1):-zand-Jare ignored when extracting; the compression is recognised automatically. - Windows: the guide's floor is Windows 10 2004 (
docs/usage/tester-image.md).Expand-Archivereads through .NET'sZipArchive, whoseIsOpenableInitialVerificationsaccepts Stored, Deflate and Deflate64 only (ZipArchiveEntry.cs). CPython'szipfile, which the workflow's verify job unpacks with, has no Deflate64 (zipfile._check_compression(9)raisesNotImplementedError). No Microsoft page lists the methods Explorer's "Extract All" reads; Deflate is the method Explorer writes, so it is the one method every documented Windows tool is known to read. - CPython 3.14's Windows binaries use zlib-ng (What's New), as does the workstation's CPython 3.14.7 (zlib-ng 2.3.3), so the zip measurements below use the runner's zlib family.
- BuildKit (
moby/buildkitmaster):util/compression/gzip.gowrites with Go'scompress/gzipat the configured level (defaultgzip.DefaultCompression) and itsNeedsConversionis false for any gzip blob, so neithercompression-levelnorforce-compressionre-compresses a gzip layer;cache/remotecache/gha/gha.gofixes the GitHub Actions cache atcompression.New(compression.Default); zstd uses klauspost/compress, levels 9 to 22 map to its best level, about zstd 11 (exporter docs).
Findings¶
Inputs: tester release tester-20261004-4d3792b3 (macOS), Windows workflow run 37190002031 (commit 8127730de), release v1.0.0-rc.2, and the published GHCR images. Times are wall clock on a 32-core host at load average 20 to 100, so only their ratios carry over.
macOS bundle (tar payload 185,152,000 bytes)¶
| Method | Bytes | Time |
|---|---|---|
published (bsdtar -czf, gzip level 6) | 69,797,228 | |
gzip -9n | 69,876,210 | 12.4 s |
zstd -19 --long=27 | 29,583,353 | 47.8 s |
zstd --ultra -22 --long=27 | 29,524,932 | 98.1 s |
xz -9 -T1 | 26,589,872 | 52.0 s |
bsdtar --options xz:compression-level=9 -cJf (chosen) | 26,583,356 | 56.8 s |
The xz archive is byte-identical across two runs, holds one xz block, unpacks in 1 s, and the unpacked tree equals the published one (diff -r). Level 9's 64 MiB window sees the previous frames of the raw test videos, which gzip's 32 KiB window cannot.
Windows zips¶
writestr(ZipInfo, data) ignores the ZipFile's compresslevel: the script's own code reproduces the zlib level 6 archive byte for byte (master-code.zip = level6.zip).
| Zip | Published | Level 6 (local) | Level 9 | zopfli, raw streams vs zlib 9 |
|---|---|---|---|---|
| x64 | 46,482,041 | 46,585,778 | 45,602,846 (3.4 s) | -3.72 %, 289 CPU s |
| arm64 | 40,883,647 | 40,978,592 | 40,131,773 (2.7 s) | -3.70 %, 252 CPU s |
| x64 CUDA | 350,615,890 | 351,365,970 | 343,773,549 (34.6 s) | -4.19 %, 3,214 CPU s |
7-Zip 26.03 Deflate -mx=9 gives 43,784,707 bytes for x64 (40 s, one thread), -mfb=257 -mpass=15 43,745,258 (145 s), Deflate64 42,903,497. Level 9 is deterministic (two runs, same bytes) and reads back in the same time as level 6.
git-archive source tarballs¶
git archive --format=tar.gz of this repository's core/ and scripts/: 5,319,106 bytes at git's default level, 5,241,283 with -9 (-1.5 %).
Release tarballs (models.tar.gz, tar 51,957,760 bytes)¶
| Method | Bytes |
|---|---|
published gzip -n (level 6; gzip -6n reproduces it exactly) | 42,249,786 |
gzip -9n (chosen) | 42,202,744 |
zstd -19 --long=27 | 41,783,286 |
xz -9 -T1 | 41,406,948 |
The models are mostly ONNX and pickle floats; no method gains more than 2 %.
Images (amd64, sum of layer sizes in bytes)¶
Each published gzip layer, decompressed and re-encoded with Go's encoders (Go 1.27 compress/gzip, klauspost/compress 1.20.1 zstd). The zstd figures equal BuildKit's: the operator image re-exported through BuildKit at compression=zstd,compression-level=3 gives the same 24,624,226 bytes.
| Image | Published | zstd 3 | zstd best |
|---|---|---|---|
vmafx-operator | 26,597,081 | 24,624,226 | 22,073,417 (-17.0 %) |
vmafx (CPU CLI) | 55,664,004 | 55,820,033 | 51,499,236 (-7.5 %) |
vmafx-server | 80,692,137 | 80,827,387 | 73,950,193 (-8.4 %) |
-cuda13 | 100,549,152 | 96,084,718 | 87,781,612 (-12.7 %) |
vmafx-node | 153,700,838 | 152,774,921 | 138,824,071 (-9.7 %) |
-tester | 267,776,859 | 229,781,834 | 200,067,558 (-25.3 %) |
-server | 283,311,726 | 266,283,042 | 234,619,966 (-17.2 %) |
-tester-cuda | 493,955,769 | 425,742,053 | 385,455,753 (-22.0 %) |
-tester-source | 690,503,022 | 690,474,747 | 690,318,003 (0.0 %) |
-tester-sycl | 935,834,881 | 917,603,114 | 884,061,331 (-5.5 %) |
-oneapi2025 | 1,424,426,946 | 1,228,663,361 | 1,073,310,596 (-24.6 %) |
-rocm10 | 8,309,870,053 | 7,410,415,655 | 7,064,598,170 (-15.0 %) |
vmafx-dev-mcp | 17,025,909,842 | 15,231,802,909 | 13,986,415,021 (-17.9 %) |
gzip at level 9 against BuildKit's default level, measured with BuildKit itself: a fresh builder (moby/buildkit:buildx-stable-1) per setting flattens the image into one new layer (FROM scratch, COPY --from) and exports it as OCI. The uncompressed layer digests (rootfs.diff_ids) are identical at every setting.
| Image, flattened | gzip default | gzip 9 | zstd 3 | zstd best |
|---|---|---|---|---|
vmafx-operator | 26,606,368 | 26,498,835 (-0.4 %) | 24,529,210 | 21,945,660 (-17.5 %) |
-tester | 264,907,857 | 264,175,167 (-0.3 %) | 226,670,199 | 197,012,637 (-25.6 %) |
On the tester the export took 46 s at the default level and 77 s at level 9 under load. Go's encoders, one thread, on the macOS tar: gzip default 25.7 MB/s, gzip 9 5.4 MB/s, zstd 3 192 MB/s, zstd best 5.7 MB/s.
After the maintainer's choice (ADR-1594)¶
The maintainer chose zstd for every image (with a Docker 23.0 floor), zopfli for the zips and zstd for the dev image. Checked before switching:
What force-compression does with zstd. util/compression/zstd.go: zstdType.NeedsConversion is true for every layer type that is not zstd, so with force-compression=true BuildKit converts gzip base-image layers and the gzip layers the GitHub Actions cache returns. Exporting the operator image through a fresh docker-container builder with compression=zstd,compression-level=22,force-compression=true,oci-mediatypes=true gave 14 of 14 layers application/vnd.oci.image.layer.v1.tar+zstd and 22,073,417 bytes, exactly the Go encoders' figure in the table above. Without oci-mediatypes, BuildKit's toDockerLayerType maps zstd to application/vnd.docker.image.rootfs.diff.tar.zstd (util/compression/compression.go), the media type docker/cli#5011 fails on.
Who pulls it. The zstd operator image, pushed to a local registry:2 and pulled by docker:<v>-dind daemons (vfs storage driver, the classic image store):
| Runtime | Pull | Run --version | docker load of the type=docker export |
|---|---|---|---|
Docker 20.10.24 (Debian 12's docker.io version) | fails: failed to register layer: ApplyLayer exit status 1 stdout: stderr: archive/tar: invalid tar header | docker: failed to register layer: ... invalid tar header. | not tried |
| Docker 23.0.6 | ok | v1.0.0-rc.2 | ok |
| Docker 28.5.2 | ok | v1.0.0-rc.2 | ok |
| Docker 29.8.2, containerd image store (this host) | ok | v1.0.0-rc.2 | |
| Podman 6.1.3 | ok | v1.0.0-rc.2 |
Syft v1.51.1 scanning the zstd and the gzip copy from the registry finds the same 186 packages; the SPDX documents differ only in the source version (the tag). The publishing workflows pull with the hosted runners' Docker (28 or later) and attest with the same Syft.
Encode time. Go's encoders (the BuildKit ones) on the first 1.5 GB of the dev container's largest layer (4.72 GB gzip, about 11 GB raw), one stream, load average about 3:
| Encoder | Output | Throughput |
|---|---|---|
| gzip default (today's dev image) | 434,922,434 | 185 MB/s |
| zstd 3 | 375,956,834 | 748 MB/s |
| zstd 8 | 351,758,617 | 399 MB/s |
| zstd 22 (best) | 328,091,166 (-24.6 %) | 64 MB/s |
The whole dev image (amd64, 52 layers, 41.0 GB raw) is 17,025,909,842 bytes as published and 13,986,415,021 with zstd best (-17.9 %; zstd 3: 15,231,802,909). At 64 MB/s the largest layer takes about 3 minutes to encode against 1 minute today, and the four largest layers (4.72, 4.25, 4.16 and 2.52 GB gzip) encode in parallel on the runner's four cores; the push is 3 GB smaller. The dev job used 57 of its 90 minutes on its last successful run (37088932576); the estimate is 58 to 62 minutes.
zopfli on the real zips, through the builder's own pack() (32 threads):
| Zip | zlib 9 | zopfli | CPU |
|---|---|---|---|
| arm64 | 40,131,773 | 38,655,329 (-3.68 %) | 347 s |
| x64 | 45,602,846 | 43,911,960 (-3.71 %) | 357 s |
| x64 CUDA | 343,773,549 | 329,387,580 (-4.18 %) | 3,215 s |
On a 4-vCPU runner the CUDA zip's 3,215 CPU seconds are about 14 minutes; its job took 10 of its 150 minutes (run 37190002031). Every zopfli zip passes unzip -t, 7z t, bsdtar -t and zipfile.testzip(), and .NET 9's ZipFile.ExtractToDirectory (what Expand-Archive uses) unpacks each to a tree equal to the published one; on those trees licensing.py check (artifacts windows-zip, windows-cuda-zip) exits 0 and the x64 SPDX document is unchanged. 50 iterations instead of 15 make a 25 MB sample of the x64 zip 0.09 % smaller for 2.9 times the CPU. zopfli releases the GIL: eight threads run eight compressions about eight times faster than one.
Alternatives explored¶
- zstd layers for every image (chosen by ADR-1594 with a Docker 23.0 floor): 5 to 25 % smaller and faster to decompress, but a Docker Engine before 23.0 cannot pull them. Debian 12's own
docker.iois 20.10.24, and every guide that names a consumer says "Docker" without a version (docs/usage/tester-image.md,docs/usage/docker.md,docs/backends/operator.md,docs/usage/storage.md,docs/server/grpc.md). Left to the maintainer: switching is one value per workflow once a floor is documented. - zopfli or 7-Zip Deflate for the zips (zopfli chosen by ADR-1594, inside the builder's own writer): 3.7 to 4.2 % smaller than zlib 9 in the same format.
zipfilecannot store pre-compressed data, so either would need a second zip writer or an external tool whose version moves with the runner image, plus a hash-locked build dependency on both Windows architectures and about 54 CPU minutes for the CUDA zip. - xz for the release tarballs: 2 % on
models.tar.gz, butxz-utilsis not Essential on Debian, so a minimal system'star -xffails, and every release document and the licence record namemodels.tar.gz. - zstd for the macOS bundle: 11 % larger than xz here, and Apple's libarchive is built without zstd.
Open questions¶
- The BuildKit encode time of zstd 22 on the largest layers has not been timed on a hosted runner; the first runs after ADR-1594 show it (dev container: 57 of 90 minutes before; ROCm 10 production: 23 of 90; the Windows CUDA zip: 10 of 150).
- Whether Explorer's "Extract All" on Windows 11 reads zip methods beyond Deflate is not documented by Microsoft; it does not matter while Windows 10 is supported.