extract: avoid refetching/reparsing repeated chunks, fixes #1678 - #9975
Conversation
A content data stream may reference the same chunk many times (e.g. the all-zero chunks of a sparse file converge on the same ids). Previously, every repetition was decrypted, authenticated and decompressed again. Add a small LRU cache of recently parsed chunks to DownloadPipeline, keyed on (id, ro_type), so repeated chunks are served from the cache. The repository is still asked for every occurrence (a cheap cached-pack slice for local repos, and it keeps the legacy remote repos' pipelining in borg transfer intact), only the parsing work is skipped. Extracting a file with a 256 MiB hole now parses the all-zero chunk once instead of 63 times. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…up#1678 The holes of a sparse file produce long runs of references to the same all-zero chunk. Instead of fetching that chunk from the repository over and over, detect it and serve it directly from the zeros constant. Detection compares the chunk id against the id of an all-zero chunk of the same size, memoized in the already existing zero_chunk_ids mapping (now shared with the create side via the new zero_chunk_id() function). To bound the memoized id computations to a few chunk sizes, they are only done for ids occurring repeatedly within the requested stream - a repeated id means repeating plaintext, which usually is a run of zeros. Unique ids are only compared against already memoized sizes. Extracting a file with a 256 MiB hole now does not access the repository at all for the hole (before: 63 object fetches). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #9975 +/- ##
==========================================
+ Coverage 85.83% 85.85% +0.02%
==========================================
Files 95 95
Lines 17034 17059 +25
Branches 2607 2611 +4
==========================================
+ Hits 14621 14646 +25
Misses 1673 1673
Partials 740 740 ☔ View full report in Codecov by Harness. |
|
Benchmark:
~7.3x faster wall clock, ~10x less user CPU. The 50 GiB file is one giant hole, archived with Before, the ~25 s of user CPU is spent decrypting and lz4-decompressing the same zero chunk ~12,800 times (about 50 GiB of decompression output). After, those chunks involve no repository access and no parsing; the remaining ~4 s is mostly interpreter/repo-open startup plus extract's per-chunk |
Extracting a sparse file repeatedly fetched and decrypted the same all-zero chunk, once per occurrence in the item's chunk list (see #1678, from 2016). A 258 MiB file with a 256 MiB hole fetched and parsed (decrypt + authenticate + decompress) the same chunk 63 times.
Two changes to
DownloadPipeline.fetch_many:1. Small LRU cache of recently parsed chunks (
parsed_cache, 4 entries, keyed(id, ro_type))Repeated chunks are parsed only once; repetitions are served from the cache. The repository is still asked for every occurrence — for local repos that is a cheap slice from the cached pack, and it keeps the request stream to legacy remote repositories (pipelined
get_manyduringborg transfer --from-borg1) unchanged. This helps any kind of repeated chunk, not just zeros.2. Serve all-zero chunks directly from the
zerosconstant, without repository accessA chunk is recognized as all-zero by comparing its id against the (memoized) id of an all-zero chunk of the same size. The memo is the already existing
zero_chunk_idsmapping, now shared with the create side via the newzero_chunk_id()helper (refactored out ofcached_hash). To bound the memoized id computations to a few chunk sizes, they are only done for ids occurring repeatedly within the requested stream — a repeated id means repeating plaintext, which usually is a run of zeros. Unique ids are only compared against already-memoized sizes (cheap dict lookup).Effect (instrumented
extract --sparseof the 256 MiB-hole file, aes256-ocb repo): the all-zero chunk went from 63 repository requests + 63 parses to 0 requests + 0 parses; totalget_manyrequests for the extract dropped from 68 to 5. Extracted content verified byte-identical, holes still punched.Notes:
ChunkListEntrylists), so the archive metadata stream path (unpack_many) is unaffected.🤖 Generated with Claude Code