analyze: deduplicated size of a set of archives, #5741 - #9971
Open
ThomasWaldmann wants to merge 7 commits into
Open
analyze: deduplicated size of a set of archives, #5741#9971ThomasWaldmann wants to merge 7 commits into
ThomasWaldmann wants to merge 7 commits into
Conversation
For the considered (matching) set of archives, `borg analyze` now reports: - deduplicated size of the set: summed plaintext size of the union of chunks the considered archives reference (chunks shared within the set counted once); - exclusive size of the set: summed plaintext size of chunks referenced only by the set and by no other archive - i.e. the space deleting the whole set frees. Following ThomasWaldmann's proposal in borgbackup#5741, chunks are flagged as referenced by the considered set and/or by the rest of the archives in a single in-memory ChunkIndex that is never written back; the exclusive size is sum(size(C - R)). Chunk ids and sizes are read from the per-archive references cache maintained by `borg compact`, so unchanged archives usually need not be opened. The reference cache helpers (get_archive_references and friends, ArchiveReferences) are moved from archiver/compact_cmd.py into cache.py as free functions so both commands share them; compact keeps its previous behavior. The dedup-size report works for one or more matching archives; the existing directory hot-spot report still requires at least two. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
borg info no longer computes per-archive deduplicated sizes (Archive.calc_stats sets usize=0 as it is expensive), and the output only shows the original size. The epilog still explained "this archive vs all archives deduplicated size", which no longer matches what the command prints. Replace it with a note that deduplicated sizes are not shown here and point to `borg analyze` (deduplicated size of a set of archives) and `borg compact --stats` (repository-wide deduplicated size). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #9971 +/- ##
==========================================
- Coverage 85.70% 82.79% -2.92%
==========================================
Files 95 95
Lines 16815 17021 +206
Branches 2577 2607 +30
==========================================
- Hits 14411 14092 -319
- Misses 1668 2191 +523
- Partials 736 738 +2 ☔ View full report in Codecov by Harness. |
ThomasWaldmann
commented
Jul 30, 2026
ThomasWaldmann
commented
Jul 30, 2026
…kup#5741 Instead of building a separate ChunkIndex, reuse the repository's own chunk index (repository.chunks): it already maps every chunk id to its stored (compressed) object size (obj_size). We OR in the ephemeral considered/rest membership flag bits and fill in the plaintext size (0 in the repo index) from the per-archive references cache, preserving pack_id/obj_offset/obj_size via _replace. These in-memory mutations are never persisted: write_chunkindex_to_repo zeroes flags and size, and close() only serializes F_NEW entries (none here). The report now shows both plaintext (uncompressed source) and stored (compressed, as stored in the repository) sizes, for both the deduplicated size of the set and its exclusive size. Chunks referenced by an archive but absent from the repo index are counted and warned about. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The considered set plus the rest covers all non-deleted archives, so chunks that end up with neither membership flag are referenced by no non-deleted archive at all - exactly what `borg compact` could free in the current state of the repo. Report their count and stored size; verified against `borg compact --stats`, which deletes precisely the reported number of objects and bytes. Only the stored size is reported for them (shown as n/a in the plaintext column): a chunk's plaintext size is not in the repository chunk index, it is only recorded in the archives that reference the chunk, and these chunks have no referencing archive left. Note that chunks of soft-deleted archives count as unreferenced here (soft-delete keeps the archive's reference cache and chunks, and compact only keeps them when the repository is damaged, so that `borg undelete` stays possible). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…out, borgbackup#5741 --by-series decomposes the whole repository by archive series: one row per series showing what is exclusive to it (no other series references those chunks, so deleting the series would free them), one row for the chunks shared by 2+ series and one row for the unreferenced chunks. Every chunk is counted in exactly one row, so the rows add up to the repository's deduplicated size. This answers "which series costs how much and what would dropping it give back" for all series at once. It needs only a single pass over the archives: bits 5..22 of the ephemeral user flags hold the series that first referenced a chunk (as index + 1, so 0 means unreferenced) and bit 23 marks chunks referenced by more than one series. As the shared and unreferenced rows require looking at every archive, --by-series always covers the whole repository and rejects archive filters. Without an archive filter, the considered set is all archives, so every referenced chunk is trivially exclusive to it and that line only repeated the deduplicated size. Suppress it in that case and label the report as the whole repository. Also factors the marking pass out into mark_references(), shared by both modes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Show stored / plaintext per row, formatted as 1.00 / 0.xx, in both the set report and the by-series decomposition. It makes the relation of the two size columns directly readable: ~0.41 for lz4-compressed text, ~1.00 for incompressible data. Rows without a plaintext size show n/a: the unreferenced chunks (their plaintext size is only recorded in the archives referencing them, and there are none), and any row whose chunks are all archive metadata (recorded with size 0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…5741 "source" says what the size is (the source data size) rather than how it is encoded, and "compression" names what the factor expresses. In the by-series table, the first column holds series names, so "name" fits better than "series". Widens the factor column to fit the longer heading and aligns the legend labels. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the request in #5741: report the deduplicated size of a set of archives (e.g. all backups matching
-a 'sh:userA-*'), following ThomasWaldmann's proposal there.Every figure is reported as source (uncompressed source data) and stored (compressed, as stored in the repository) size, plus the compression factor relating the two (stored / source).
Deduplicated size of a set of archives
For the considered (matching) set of archives:
borg compactcould free in the current state of the repository. Only their stored size is known: a chunk's source size is only recorded in the archives referencing it, and these have none.Following the issue comment, chunks are flagged as referenced by the considered set (C) and/or by the rest of the archives (R); the exclusive size is
sum(size(x) for x in C - R).Without an archive filter, the considered set is all archives - every referenced chunk is then trivially exclusive to it, so that line is suppressed and the report is labeled as the whole repository.
Decomposition by archive series (
--by-series)Every chunk is counted in exactly one row, so the rows add up to the repository's deduplicated size. This answers "which series costs how much, and what would I get back by dropping it" for all series at once:
This needs only a single pass over the archives: bits 5..22 of the ephemeral user flags hold the series that first referenced a chunk (as index + 1, so 0 means unreferenced), bit 23 marks chunks referenced by more than one series. As the shared and unreferenced rows can only be determined by looking at every archive,
--by-seriesalways covers the whole repository and rejects archive filters.Implementation
repository.chunks) instead of building a second index — it already holds each chunk's stored size (obj_size). The membership flag bits are OR-ed in and the source size (0 in the repo index) is filled in from the per-archive references cache, preservingpack_id/obj_offset/obj_sizevia_replace. These in-memory mutations never persist:write_chunkindex_to_repozeroes flags and size, andclose()only serializesF_NEWentries (none here).borg compact, so unchanged archives usually do not need to be opened/decrypted.get_archive_references& friends,ArchiveReferences) are moved fromarchiver/compact_cmd.pyintocache.pyas free functions soanalyzeandcompactshare one implementation;compactkeeps its previous behavior.Notes
-aneeds thesh:prefix for globs (borg2's default match is exact).borg compactonly keeps them when the repository is damaged, soborg undeletestays possible).borg infoepilog (borg infono longer computes per-archive deduplicated sizes) and points toborg analyze/borg compact --statsinstead.Tests
analyze_cmd_test.pycovers: set dedup vs exclusive size with shared and unique files, the single-archive set, the whole-repository (no filter) report, the--by-seriesdecomposition (asserting the rows add up to the total), the rejection of filters with--by-series, and the unreferenced-chunks report - including thatborg compactthen frees exactly the reported number of objects. Existing analyze/compact/cache/info suites pass.