Every number here is measured. Update payloads were captured over real HTTP sessions (cavs-server + cavs-client). Real-game figures come from open-source Godot projects exported to PCK at two real points in their git history. "Update" means what a player who already has the previous version downloads; the full-download baseline is the new release compressed with zstd -3.
A player who already has the previous version downloads only the missing chunks required to reconstruct the new release.
| Game | Update | Full (zstd) | CAVS (64 KiB) | Saved |
|---|---|---|---|---|
| godotengine/tps-demo (569 MB) | tag 4.5 → master, 7 files | 247.60 MiB | 1.64 MiB | −99.3% |
| MechanicalFlower/Marble | 1.6.0 → 1.6.1 | 6.55 MiB | 0.14 MiB | −97.9% |
| GDQuest 3D third-person | HEAD~10 → HEAD, 468 files | 27.61 MiB | 8.70 MiB | −68.5% |
Re-downloading the same version costs 0 bytes of payload — everything resolves from the persistent cache as references. All reconstructions were SHA-256 verified and byte-identical; the Godot runtime mounted the reconstructed PCKs via load_resource_pack().
cavs pack --bootstrap also emits the full artifact zstd-19-compressed. The server offers it to cold clients whenever it beats the chunk path, and the client seeds its chunk cache from it — so the next update is incremental with zero extra download. When the payload is incompressible, the server keeps the chunk path.
| Game | PCK v1 | Full zstd-3 | Dual route cold | vs zstd-3 |
|---|---|---|---|---|
| godotengine/tps-demo | 569.15 MiB | 247.62 MiB | 221.42 MiB | −10.6% |
| GDQuest 3D third-person | 61.09 MiB | 27.66 MiB | 24.43 MiB | −11.7% |
| MechanicalFlower/Marble | 9.59 MiB | 6.55 MiB | 5.68 MiB | −13.2% |
| Godot 4.7 export suite | 5.39 MiB | 4.52 MiB | 4.20 MiB | −7.1% |
The server routed all four games to the bootstrap automatically (per-session estimate vs threshold); every reconstruction was byte-identical.
When payload is already cached, the manifest becomes the cost of checking for updates — the "is there an update?" call every launcher makes. The binary manifest (CAVSMF2) stores each unique chunk hash once and references it by varint index.
| Game | JSON v1 | Binary v2 | Parse |
|---|---|---|---|
| tps-demo | 894.2 KiB | 208.5 KiB | 0.53 → 0.49 ms |
| GDQuest | 103.1 KiB | 24.8 KiB | 0.062 → 0.058 ms |
| Marble | 20.3 KiB | 5.1 KiB | 0.018 → 0.017 ms |
Chunk-path bytes are unchanged; the win lands where metadata dominates — a warm re-fetch now costs ~75% less wire.
Per-pair delta tools win on raw bytes for one exact version pair — that is inherent and expected. The point is the operational model: CAVS packages once per release and serves any version jump from one deduplicated, CDN-cacheable store, with resumable, self-repairing delivery.
| Game (v1→v2) | zstd full | rsync wire | xdelta3 -9 | bsdiff | CAVS (64k) |
|---|---|---|---|---|---|
| tps-demo | 247.60 | 462.95 | 0.03 | 0.03 | 1.64 |
| GDQuest | 27.61 | 54.86 | 3.78 | 3.82 | 8.70 |
| Marble | 6.55 | 0.02 | 0.00 | 0.00 | 0.14 |
The delivery planner prices chunks, hybrid reconstruction, offline plans, optimized sidecars, bootstrap, full download and no-op against client state, memory budget and measured cost. Below is the full external pipeline on the same 126 MiB directory release — butler's default diff and optimized rediff --rediff-quality 9, plus byte-level proxies. Every output byte-identical; RSS via /usr/bin/time on real binaries.
| Route | Download | Generate | Apply | Peak RAM |
|---|---|---|---|---|
| CAVS auto-route (plan) | 2.51 MiB | 0.54 s | 391 ms | 23 MiB |
| butler diff (default) | 2.52 MiB | 1.0 s | 331 ms | 35 MiB |
| butler rediff q9 (optimized) | 2.51 MiB | 11.6 s | 403 ms | 97 MiB |
| pairwise proxy — xdelta3 + zstd-19 | 3.01 MiB | 4.1 s | 3.4 s | 397 MiB |
| pairwise proxy — bsdiff + zstd-19 | 3.02 MiB | 25.0 s | 3.4 s | 2.3 GiB |
| full download (zstd-19) | 62.12 MiB | 3.9 s | 14 ms | — |
| Change (128 MiB artifact) | full zstd-19 | CAVS plan | butler optimized | bsdiff proxy | xdelta3 proxy |
|---|---|---|---|---|---|
| small change (~3%) | 64.05 MiB | 1.94 MiB | 1.94 MiB | 1.96 MiB | 1.94 MiB |
| shifted (every byte moves) | 64.06 MiB | 4.21 KiB | 11.39 KiB | 4.59 KiB | 4.34 KiB |
| compressed blob (62 MiB .tar.zst) | — | 2.53 MiB | 2.90 MiB | — | 2.53 MiB |
On the shifted artifact CAVS's content-defined blocks survive the unaligned insert and beat butler's fixed-block scan 16×. The compressed-blob weak case is closed: the per-file optimizer routes the high-entropy blob through a byte-level delta — 2.53 MiB where naïve block routes paid 21.9 MiB.
The v0.9.0 analyzer estimates a build transition under a public fixed-1MiB chunk model (SteamPipe-style — never Valve's implementation) and diagnoses the layouts that bloat updates. Below: the same 64 KiB of real change in a 32-asset pack, under different layouts. Deterministic datasets, reproducible with cavs bench steampipe-cases.
| Pack layout | SteamPipe-style estimate | Fixed reuse | CAVS .cavsplan | Diagnosis |
|---|---|---|---|---|
| Localized edit, stable layout | 1.00 MiB | 97.0% | 131 KiB | OK |
| 4 KiB inserted at the front | 32.88 MiB | 0.0% | 7.4 KiB | asset_shuffling |
| Same assets, shuffled order | 32.88 MiB | 0.0% | 67 KiB | asset_shuffling |
| Per-asset headers rewritten each build | 32.00 MiB | 2.7% | 2.13 MiB | toc_churn |
| Same edit, TOC centralized (the fix) | 1.88 MiB | 94.3% | 132 KiB | OK |
| Pack compressed as one stream | whole blob, 0% reuse | 0.0% | 130 KiB | compressed blob |
| Per-asset compression, padded slots (the fix) | 97 KiB | 75.0% | 68 KiB | OK |
| ~3-byte change in a 256 MiB pack | Download | Local read + write | HDD est. | NVMe est. |
|---|---|---|---|---|
| One monolithic pack | 2.00 MiB | 512 MiB | 4.7 s | 176 ms |
| Split into 8 × 32 MiB (the fix) | 2.00 MiB | 128 MiB | 1.2 s | 45 ms |
| Raw full download (reference) | 256 MiB | 256 MiB | 2.6 s | 102 ms |
On an HDD, the "smart" 2 MiB update of the monolithic pack is slower than the dumb 256 MiB full download — a fixed-chunk updater rebuilds every touched file in full. cavs io-estimate flags when local I/O dominates the network saving; splitting the pack fixes it at the same download size.
Windows ↔ Linux depots of the same build share 48.83 MiB of 49.11 MiB — the workspace prices exactly what a shared depot would save in storage and egress.
Install plans filter depots by platform, language and ownership and reuse chunks across depots: a demo owner who already installed the full game downloads nothing.
The policy planner picks the 129 KiB .cavsplan for every previous-install state — and honestly picks the raw full download for a cold install of incompressible data, because a bootstrap would save nothing.
Raw outputs and reproduction commands: docs/results/v0.9.0/ in the repository. Every SteamPipe-style figure is an estimate from public documentation, not Valve's exact SteamPipe implementation.
A player with the old build installed but an empty cache copies verified ranges straight from the old file and fetches only what changed — every range BLAKE3-checked before it is written, the final output gated by SHA-256. Measured on a 128 MiB synthetic build against a full re-download.
| Update variant | Full re-download | CAVS — cold cache + old install | Reduction |
|---|---|---|---|
| small change | 64.55 MiB | 6.24 MiB | −90.3% |
| medium change | 64.56 MiB | 26.53 MiB | −58.9% |
| shifted (every byte moves) | 64.56 MiB | 10.9 KiB | −99.98% |
Point the client at --previous-artifact. Shared ranges are copied from disk, not the network — no chunk cache required.
A deterministic old-version signature — 88 KiB for a 128 MiB build (~0.07%) — lets a new version be diffed without the old bytes.
An output that already matches costs 0 payload bytes. In directory mode, unchanged files — including player mods — are never rewritten.
Adjacent copy ranges merge: on the shifted build, 1,082 chunk ops → 18 contiguous reads, read amplification 1.0.
A deterministic 1 GiB benchmark suite (same seed ⇒ identical bytes on any machine) tests the update shapes that matter for chunked delivery. FastCDC 64 KiB + zstd 3.
| Update shape | Changed | Pack time | Update egress |
|---|---|---|---|
| Small change | ~3% of blocks | 6.4 s | 51.3 MiB |
| Medium change | ~15% | 6.8 s | 216.6 MiB |
| Large change | ~50% | 6.8 s | 449.1 MiB |
| Head-shifted | 4 KiB inserted at head — every byte shifts | 7.3 s | 10.9 KiB |
| Reordered | Same blocks, 8 MiB groups swapped | 9.1 s | 20.8 MiB |
Measured on the official Marble 1.6.0 → 1.6.1 Linux release builds (real PCK pair). The new fastcdc-16k / fastcdc-32k profiles use tight FastCDC normalization; the compact binary manifest keeps their metadata cost at a few KiB. Pack-time hashing and compression now run on all cores (3–7× faster, byte-identical output), which makes per-chunk zstd-19 practical for publishing.
| Config | Update wire | vs previous default | Cold wire |
|---|---|---|---|
| fastcdc-64k + zstd-3 (previous default) | 1.50 MiB | — | 7.81 MiB |
| fastcdc-32k + zstd-3 | 0.80 MiB | −47% | 8.01 MiB |
| fastcdc-16k + zstd-3 | 0.53 MiB | −65% | 8.12 MiB |
| fastcdc-16k + zstd-19 | 0.48 MiB | −68% | 7.50 MiB |
Shared chunks are stored once. Packfile-backed stores collapse thousands of tiny chunk objects into a few immutable .cavspack files and coalesce nearby chunk reads into a few range reads — a deterministic object tree suitable for S3, R2 or static CDN.
Two versions of Marble: 13.80 MiB logical → 7.04 MiB on disk — roughly 49% less storage, shared chunks stored once. GC reclaims unreferenced chunks.
Store + 3 policy-chosen hot-pair sidecars = 35.91 MiB serving any jump, vs 144.23 MiB for all-pairs one-hop coverage (45 patches) — 75% less. v1.1.0 also benchmarks the practical adjacent/ladder/base/hot-pair policies head-to-head.
store export emits a stable object tree for static CDN hosting — same wire bytes as loose storage, far fewer objects to host.
| Game | Chunk objects on disk | Physical reads (whole session) | Read amplification |
|---|---|---|---|
| MechanicalFlower/Marble | 130 → 4 | 130 → 2 (65×) | 1.000 |
| GDQuest 3D third-person | 807 → 4 | 805 → 7 (115×) | 1.000 |
| godotengine/tps-demo | 5,775 → 6 | 5,775 → 34 (170×) | 1.000 |
Amplification 1.000 means coalescing read zero extra bytes: chunks are written in reconstruction order, so merged ranges are exactly contiguous.
The client reconstructs by streaming to disk — batch-decoded from the socket → disk cache → .part → SHA-256 → atomic rename — so RAM stays flat regardless of game size. Measured on the tps-demo update (569 MB game, release binaries).
Resilience is measured too: an install killed with kill -9 at 57 MiB resumed with an HTTP Range request and downloaded only the remaining 166.5 MiB; a cache with 3 corrupted chunks (of 5,747) detected, quarantined and re-fetched exactly those 3, then returned to 0-byte re-fetches. A ~20-case corruption matrix and 5 libFuzzer targets leave zero unauthenticated-content survivors.
CAVS is not a codec and does not pretend to be. It is most valuable when redundancy crosses versions, releases, bundles or repeated assets.
The synthetic large-build suite is fully reproducible from the source tree — a deterministic PRNG regenerates the exact same dataset on any machine and writes summary.md / summary.json.
$ cavs bench gen --out ds --size 1GiB $ cavs bench suite --dataset ds --out results # → results/summary.md, results/summary.json
The real-game harnesses and their raw result data live in the full development repository; the measured summaries above are what those harnesses produced. Full write-ups: docs/BENCHMARKS.md, docs/ROUTE_BENCHMARKS.md, docs/STEAMPIPE_COMPARISON.md, docs/results/v0.8.0/, docs/results/v0.9.0/.