Deduplicate all files in the wheel cache - #21327
Conversation
|
N.B. Slop comment for benchmark data. We benchmarked the optimization in 85c3f7485 against the binary-only cache at a3f977ece, with
Before this optimization, the measured cold regressions were +4.02% for AnyIO 4.9.0, +19.41% for SymPy 1.14.0, +12.75% for NumPy 2.2.6, +15.30% for PyTorch 2.7.1+cpu. All eight median regressions are below 5%. The PyTorch cold 95% interval still reaches 5.18%, so its upper bound is not below 5%. Every other upper bound is below 5%. We retained all 1,860 measured installs, including outliers, and combined every confirmation run for this candidate. Cold measurements have 360 paired rounds for AnyIO, 120 each for SymPy and NumPy, and 90 for PyTorch; warm measurements have 60 paired rounds per wheel. Baseline/candidate order alternates, with three warmups per case. Intervals use a paired percentile bootstrap of the ratio of medians with 10,000 resamples. These measurements ran on Linux/ext4, an AMD EPYC-Milan VM pinned to eight CPUs, with Python 3.12.13. Both binaries use Rust 1.98.0 and the same All-file deduplication, content/executable identities, the cache layout, complete archives, and copy fallbacks are preserved. Inode checks confirmed that every archived file shares its file-store object for all four wheels. The five targeted integration tests passed ten stress iterations (50 executions), including local and streamed wheels with one and four workers, RECORD handling, cache cleanup, and cross-filesystem installation. Formatting and Clippy with warnings denied also passed. |
Merging this PR will not alter performance
Comparing Footnotes
|
a3f977e to
1f28fd5
Compare
0a8edc0 to
a108fab
Compare
uv test inventory changesThis PR changes the tests when compared with the
|
## Summary When content hashing is enabled, we currently allocate and zero a new 64 KiB buffer for every file we copy and hash during streaming extraction. This PR reuses one buffer across the wheel instead. For the PyTorch wheel used in the benchmarks, that reduces buffer allocations for hashing from 11,120 to one, while keeping the buffer size at 64 KiB per active wheel. The following measurements compare #21327 at `a188b8e833aef3c3b4b60a32ed9fafe6ac74186a` with this optimization applied on top, before moving the change onto `main`. They are not measurements against `main`. The Linux benchmarks alternate base and candidate, using pinned wheels served over local HTTP with content-addressed caching enabled: | Cold install | #21327 | #21327 + buffer reuse | Change | | --- | ---: | ---: | ---: | | AnyIO | 110 ms | 107 ms | -2.6% | | SymPy | 845 ms | 775 ms | -8.3% | | NumPy | 627 ms | 567 ms | -9.5% | | PyTorch CPU | 6.50 s | 5.99 s | -7.8% | | 14-package environment, concurrency 4 | 6.95 s | 6.47 s | -7.0% | The individual results above use 16 paired rounds; the full environment uses 12. AnyIO, SymPy, and NumPy were repeated after an initial 20-pair run: the initial AnyIO timings were noisy, while the initial SymPy and NumPy improvements were 7.8% and 6.9%. All original samples were retained. Cached installs and local-wheel controls showed no consistent change. Across the initial runs, repeats, and controls, we measured 672 installs, excluding warmups and cache priming. Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
a188b8e to
694af80
Compare
|
Okay, I experimented with a bunch of alternatives to try and make this more performant:
I also considered something like "guess whether a file is executable" during extraction (and then, if we're wrong, copy it and change the mode after unzipping). I guess this could end up being more performant, but I wasn't very happy with the heuristics. Ultimately, I think what we have here is good. |
73c621b to
87bd56f
Compare
## Summary Follow-up to #21327. Cache cleanup currently reads the hardlink count of every `files-v0` object separately. On macOS, we can request names, file types, and link counts in batches with `getattrlistbulk`, allocating paths only for files with a single link. Use this fast path for flat cache shards. We keep the existing walk on other platforms, when bulk reads or required attributes aren't available, and for nested directories. We don't follow symlinks, and file removal still uses the existing storage accounting. On macOS, with 87,129 distinct empty file objects across 256 shards and two hardlinks per object, median full-command times across eight paired warm-cache runs were: | Command | Before | After | | ------------------------------ | -----: | -----: | | `uv cache clean <package>` | 386 ms | 105 ms | | `uv cache clean <10 packages>` | 389 ms | 104 ms | | `uv cache prune` | 386 ms | 101 ms | These runs retain every object, so they measure scan cost rather than deletion throughput. The scan is still single-threaded. --------- Co-authored-by: Zanie Blue <contact@zanie.dev>
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [ghcr.io/astral-sh/uv](https://github.com/astral-sh/uv) | final | patch | `0.12.7` → `0.12.8` | --- >⚠️ **Warning** > > Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/51) for more information. --- ### Release Notes <details> <summary>astral-sh/uv (ghcr.io/astral-sh/uv)</summary> ### [`v0.12.8`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#0128) Released on 2026-08-31. ##### Enhancements - Warn about invalid tool directories and continue upgrading valid tools with `uv tool upgrade --all` ([#​21368](astral-sh/uv#21368)) ##### Preview features - Deduplicate identical files within and across cached wheels with the `content-addressed-cache` preview feature ([#​21327](astral-sh/uv#21327)) - Reduce allocations while extracting content-addressed wheels by reusing the hashing buffer across files ([#​21340](astral-sh/uv#21340)) - Speed up content-addressed cache cleanup on macOS by reading hard-link counts in bulk ([#​21344](astral-sh/uv#21344)) ##### Performance - Prevent concurrent uv processes from downloading and extracting the same remote wheel more than once ([#​21379](astral-sh/uv#21379)) - Speed up dependency graph construction from large lockfiles by indexing packages during traversal ([#​21373](astral-sh/uv#21373)) - Extend indexed lockfile traversal to exports, dependency trees, audits, and freshness checks ([#​21377](astral-sh/uv#21377)) - Speed up warm resolutions by reducing repeated marker interner work ([#​21300](astral-sh/uv#21300)) ##### Bug fixes - Do not trust hashes from direct URLs discovered only in wheel metadata when installing with `--require-hashes` ([#​21348](astral-sh/uv#21348)) - Use a compatible Azure Storage API version for anonymous and authenticated requests, allowing credential retries when public access is disabled ([#​21366](astral-sh/uv#21366)) - Redact Azure shared access signature (`sig`) query parameters from displayed URLs ([#​21360](astral-sh/uv#21360)) - Treat projects below one-level workspace member globs as standalone instead of aborting workspace discovery ([#​21341](astral-sh/uv#21341)) ##### Other changes - Update `astral-tokio-tar` to 0.7.0 and use effective sizes when tracking extracted hard links ([#​21346](astral-sh/uv#21346)) </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC41Mi4wIiwidXBkYXRlZEluVmVyIjoiNDQuNTIuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsidHlwZS9kZXBlbmRlbmNpZXMiXX0=--> Reviewed-on: https://git.tainton.uk/repos/labmcp/pulls/52 Co-authored-by: renovate[bot] <renovate-bot@git.tainton.uk>
Automated mise tool upgrades from local config. mise-managed tools: - `action-validator` - `actionlint` - `aube` - `editorconfig-checker` - `ghalint` - `pinact` - `pipx:gh-action-pulse` - `prek` - `rumdl` - `shellcheck` - `shfmt` - `tombi` - `uv` - `yamlfmt` - `yamllint` - `zizmor` Command: `mise upgrade --bump --local action-validator actionlint aube editorconfig-checker ghalint pinact pipx:gh-action-pulse prek rumdl shellcheck shfmt tombi uv yamlfmt yamllint zizmor` <details> <summary>Version changelog (2 tools)</summary> | Tool | Requested | Installed | |------|-----------|-----------| | `prek` | `0.5.0` → `0.5.1` | `0.5.0` → `0.5.1` | | `uv` | `0.12.7` → `0.12.8` | `0.12.7` → `0.12.8` | </details> <details> <summary>Release notes (2 tools)</summary> <details> <summary>prek: `0.5.0` → `0.5.1` (j178/prek)</summary> ### v0.5.1 ## Release Notes Released on 2026-09-01. ### Enhancements - Add `--hide-status <passed|failed|skipped>` for hook reports ([#2644](j178/prek#2644)) - Add `prek init` for repository setup ([#2636](j178/prek#2636)) - Apply hook `env` during environment creation ([#2650](j178/prek#2650)) - Disable error snippets in `check-yaml` diagnostics ([#2664](j178/prek#2664)) - Show hooks excluded by skip selectors ([#2645](j178/prek#2645)) - Support Pixi for Conda environments ([#2667](j178/prek#2667)) - Support `cargo-binstall` for Rust CLI dependencies ([#2658](j178/prek#2658)) - Warn about unused keys in user settings ([#2665](j178/prek#2665)) ### Bug fixes - Reject unsupported YAML tags in check-yaml ([#2656](j178/prek#2656)) ### Documentation - Clarify local hook documentation ([#2640](j178/prek#2640)) - Clarify pre-commit command compatibility ([#2635](j178/prek#2635)) - Document automatic PR fixes with autofix.ci ([#2643](j178/prek#2643)) - Document check-yaml unsafe support ([#2632](j178/prek#2632)) - Improve setup and workflow documentation ([#2637](j178/prek#2637)) ### Other changes - Drop low-usage release targets ([#2651](j178/prek#2651)) - Generate a prek manifest JSON schema ([#2648](j178/prek#2648)) ### Contributors - @j178 ## Install prek 0.5.1 ### Install prebuilt binaries via shell script ```sh curl --proto '=https' --tlsv1.2 -LsSf https://github.com/j178/prek/releases/download/v0.5.1/prek-installer.sh | sh ``` ### Install prebuilt binaries via powershell script ```sh powershell -ExecutionPolicy Bypass -c "irm https://github.com/j178/prek/releases/download… (truncated) </details> <details> <summary>uv: `0.12.7` → `0.12.8` (astral-sh/uv)</summary> ### 0.12.8 ## Release Notes Released on 2026-08-31. ### Enhancements - Warn about invalid tool directories and continue upgrading valid tools with `uv tool upgrade --all` ([#21368](astral-sh/uv#21368)) ### Preview features - Deduplicate identical files within and across cached wheels with the `content-addressed-cache` preview feature ([#21327](astral-sh/uv#21327)) - Reduce allocations while extracting content-addressed wheels by reusing the hashing buffer across files ([#21340](astral-sh/uv#21340)) - Speed up content-addressed cache cleanup on macOS by reading hard-link counts in bulk ([#21344](astral-sh/uv#21344)) ### Performance - Prevent concurrent uv processes from downloading and extracting the same remote wheel more than once ([#21379](astral-sh/uv#21379)) - Speed up dependency graph construction from large lockfiles by indexing packages during traversal ([#21373](astral-sh/uv#21373)) - Extend indexed lockfile traversal to exports, dependency trees, audits, and freshness checks ([#21377](astral-sh/uv#21377)) - Speed up warm resolutions by reducing repeated marker interner work ([#21300](astral-sh/uv#21300)) ### Bug fixes - Do not trust hashes from direct URLs discovered only in wheel metadata when installing with `--require-hashes` ([#21348](astral-sh/uv#21348)) - Use a compatible Azure Storage API version for anonymous and authenticated requests, allowing credential retries when public access is disabled ([#21366](astral-sh/uv#21366)) - Redact Azure shared access signature (`sig`) query parameters from displayed URLs ([#21360](astral-sh/uv#21360)) - Treat projects below one-level workspace member globs as standalone instead of aborting workspace discovery ([#21341](https://github.com/astra… (truncated) </details> </details> Modified files: - `.mise.toml`
Automated mise tool upgrades from local config. mise-managed tools: - `action-validator` - `actionlint` - `aube` - `editorconfig-checker` - `ghalint` - `pinact` - `pipx:gh-action-pulse` - `prek` - `rumdl` - `shellcheck` - `shfmt` - `tombi` - `uv` - `yamlfmt` - `yamllint` - `zizmor` Command: `mise upgrade --bump --local action-validator actionlint aube editorconfig-checker ghalint pinact pipx:gh-action-pulse prek rumdl shellcheck shfmt tombi uv yamlfmt yamllint zizmor` <details> <summary>Version changelog (2 tools)</summary> | Tool | Requested | Installed | |------|-----------|-----------| | `prek` | `0.5.0` → `0.5.1` | `0.5.0` → `0.5.1` | | `uv` | `0.12.7` → `0.12.8` | `0.12.7` → `0.12.8` | </details> <details> <summary>Release notes (2 tools)</summary> <details> <summary>prek: `0.5.0` → `0.5.1` (j178/prek)</summary> ### v0.5.1 ## Release Notes Released on 2026-09-01. ### Enhancements - Add `--hide-status <passed|failed|skipped>` for hook reports ([#2644](j178/prek#2644)) - Add `prek init` for repository setup ([#2636](j178/prek#2636)) - Apply hook `env` during environment creation ([#2650](j178/prek#2650)) - Disable error snippets in `check-yaml` diagnostics ([#2664](j178/prek#2664)) - Show hooks excluded by skip selectors ([#2645](j178/prek#2645)) - Support Pixi for Conda environments ([#2667](j178/prek#2667)) - Support `cargo-binstall` for Rust CLI dependencies ([#2658](j178/prek#2658)) - Warn about unused keys in user settings ([#2665](j178/prek#2665)) ### Bug fixes - Reject unsupported YAML tags in check-yaml ([#2656](j178/prek#2656)) ### Documentation - Clarify local hook documentation ([#2640](j178/prek#2640)) - Clarify pre-commit command compatibility ([#2635](j178/prek#2635)) - Document automatic PR fixes with autofix.ci ([#2643](j178/prek#2643)) - Document check-yaml unsafe support ([#2632](j178/prek#2632)) - Improve setup and workflow documentation ([#2637](j178/prek#2637)) ### Other changes - Drop low-usage release targets ([#2651](j178/prek#2651)) - Generate a prek manifest JSON schema ([#2648](j178/prek#2648)) ### Contributors - @j178 ## Install prek 0.5.1 ### Install prebuilt binaries via shell script ```sh curl --proto '=https' --tlsv1.2 -LsSf https://github.com/j178/prek/releases/download/v0.5.1/prek-installer.sh | sh ``` ### Install prebuilt binaries via powershell script ```sh powershell -ExecutionPolicy Bypass -c "irm https://github.com/j178/prek/releases/download… (truncated) </details> <details> <summary>uv: `0.12.7` → `0.12.8` (astral-sh/uv)</summary> ### 0.12.8 ## Release Notes Released on 2026-08-31. ### Enhancements - Warn about invalid tool directories and continue upgrading valid tools with `uv tool upgrade --all` ([#21368](astral-sh/uv#21368)) ### Preview features - Deduplicate identical files within and across cached wheels with the `content-addressed-cache` preview feature ([#21327](astral-sh/uv#21327)) - Reduce allocations while extracting content-addressed wheels by reusing the hashing buffer across files ([#21340](astral-sh/uv#21340)) - Speed up content-addressed cache cleanup on macOS by reading hard-link counts in bulk ([#21344](astral-sh/uv#21344)) ### Performance - Prevent concurrent uv processes from downloading and extracting the same remote wheel more than once ([#21379](astral-sh/uv#21379)) - Speed up dependency graph construction from large lockfiles by indexing packages during traversal ([#21373](astral-sh/uv#21373)) - Extend indexed lockfile traversal to exports, dependency trees, audits, and freshness checks ([#21377](astral-sh/uv#21377)) - Speed up warm resolutions by reducing repeated marker interner work ([#21300](astral-sh/uv#21300)) ### Bug fixes - Do not trust hashes from direct URLs discovered only in wheel metadata when installing with `--require-hashes` ([#21348](astral-sh/uv#21348)) - Use a compatible Azure Storage API version for anonymous and authenticated requests, allowing credential retries when public access is disabled ([#21366](astral-sh/uv#21366)) - Redact Azure shared access signature (`sig`) query parameters from displayed URLs ([#21360](astral-sh/uv#21360)) - Treat projects below one-level workspace member globs as standalone instead of aborting workspace discovery ([#21341](https://github.com/astra… (truncated) </details> </details> Modified files: - `.mise.toml` Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [uv](https://github.com/astral-sh/uv) | tools | patch | `0.12.7` → `0.12.9` | --- >⚠️ **Warning** > > Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/141) for more information. --- ### Release Notes <details> <summary>astral-sh/uv (uv)</summary> ### [`v0.12.9`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#0129) [Compare Source](astral-sh/uv@0.12.8...0.12.9) Released on 2026-09-01. ##### Python - Add CPython 3.15.0rc2 ([#​21413](astral-sh/uv#21413), [#​21415](astral-sh/uv#21415)) ##### Enhancements - Add `--no-locked` and `--no-frozen` to disable lock modes enabled by `UV_LOCKED` and `UV_FROZEN` for a single invocation ([#​21408](astral-sh/uv#21408)) - Report the exact command-line lock-mode flag in warnings and errors ([#​21402](astral-sh/uv#21402)) ##### Performance - Speed up cold wheel installs by extracting each streaming ZIP archive in a single blocking task and reusing buffers across files ([#​21372](astral-sh/uv#21372)) ##### Bug fixes - Update `async_http_range_reader` to 0.11.1 to address a potential memory-safety issue when reading metadata ranges from untrusted wheels ([#​21401](astral-sh/uv#21401)) - Remove sensitive headers when redirects cross authentication realms, including same-host redirects that change URL schemes ([#​21382](astral-sh/uv#21382)) - Redact secrets in signed URLs from retry diagnostics, including nested request errors ([#​21381](astral-sh/uv#21381)) - Give `--locked`, `--frozen`, `--check`, and `--check-exists` precedence over conflicting `UV_LOCKED` and `UV_FROZEN` values ([#​21396](astral-sh/uv#21396)) - Prevent concurrent uv processes from redundantly extracting the same local or source-built wheel ([#​21400](astral-sh/uv#21400)) ### [`v0.12.8`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#0128) [Compare Source](astral-sh/uv@0.12.7...0.12.8) Released on 2026-08-31. ##### Enhancements - Warn about invalid tool directories and continue upgrading valid tools with `uv tool upgrade --all` ([#​21368](astral-sh/uv#21368)) ##### Preview features - Deduplicate identical files within and across cached wheels with the `content-addressed-cache` preview feature ([#​21327](astral-sh/uv#21327)) - Reduce allocations while extracting content-addressed wheels by reusing the hashing buffer across files ([#​21340](astral-sh/uv#21340)) - Speed up content-addressed cache cleanup on macOS by reading hard-link counts in bulk ([#​21344](astral-sh/uv#21344)) ##### Performance - Prevent concurrent uv processes from downloading and extracting the same remote wheel more than once ([#​21379](astral-sh/uv#21379)) - Speed up dependency graph construction from large lockfiles by indexing packages during traversal ([#​21373](astral-sh/uv#21373)) - Extend indexed lockfile traversal to exports, dependency trees, audits, and freshness checks ([#​21377](astral-sh/uv#21377)) - Speed up warm resolutions by reducing repeated marker interner work ([#​21300](astral-sh/uv#21300)) ##### Bug fixes - Do not trust hashes from direct URLs discovered only in wheel metadata when installing with `--require-hashes` ([#​21348](astral-sh/uv#21348)) - Use a compatible Azure Storage API version for anonymous and authenticated requests, allowing credential retries when public access is disabled ([#​21366](astral-sh/uv#21366)) - Redact Azure shared access signature (`sig`) query parameters from displayed URLs ([#​21360](astral-sh/uv#21360)) - Treat projects below one-level workspace member globs as standalone instead of aborting workspace discovery ([#​21341](astral-sh/uv#21341)) ##### Other changes - Update `astral-tokio-tar` to 0.7.0 and use effective sizes when tracking extracted hard links ([#​21346](astral-sh/uv#21346)) </details> --- ### Configuration 📅 **Schedule**: (in timezone Europe/London) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC41Mi4xIiwidXBkYXRlZEluVmVyIjoiNDQuNTYuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsicmVub3ZhdGUvZ2l0aHViLXJlbGVhc2UiLCJ0eXBlL3BhdGNoIl19--> Reviewed-on: https://forgejo.hayden.moe/hayden/phoebe/pulls/430
Summary
On main, we support content-addressed caching, but only at the wheel-level. That is, if you download the same wheel twice from different sources, they share a cache entry. But files within or across wheels are not deduplicated at all.
This PR adds deduplication at the file level: every file is now stored under its BLAKE3 hash in a
files-v0bucket. We hardlink these objects into their original locations inarchive-v0, so the installation step doesn't change at all -- we're just deduping within the cache (and cache cleanup removes file objects when their hardlink count drops to one).In the prior proposal (#19694), we included the following table:
files-v0objectsSo we're saving 545.2 MiB on my local machine, or about 10% of the cache.
In return, the net effect seems to be something like a <4% slowdown for cold installs (and no effect on warm installs), which I think is probably worthwhile here.