Repository navigation
Count blocks shared by APFS clones once - #19
Conversation
st_blocks reports every block a clone shares with its siblings, so a file cloned N times was counted N times in allocated size. On my Mac, cloned files in WhatsApp's container pushed the reported total above the capacity of the 1 TB drive. getattrlistbulk now also requests ATTR_CMNEXT_PRIVATESIZE, CLONEID and CLONE_REFCNT. Each clone keeps its private blocks; the shared blocks are charged to the first member of its clone group seen in the scan. Logical size is unchanged and still reports every copy in full. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
phalladar
left a comment
There was a problem hiding this comment.
Thanks for this. Clone double-counting is a real problem, and charging the shared blocks once per clone group matches how the scanner already treats hard links. Two changes before merging:
1. Drop ATTR_CMNEXT_PRIVATESIZE: it makes the bulk scan ~70% slower and isn't needed.
compareDirectoryEnumerationPerformance on /Applications (387k files, macOS 27, .automatic mode, two runs each):
| Build | Elapsed |
|---|---|
| main | 1.42–1.46 s |
| this PR | 2.32–2.53 s (about the same as the POSIX fallback's 2.4–2.9 s) |
this PR without ATTR_CMNEXT_PRIVATESIZE |
1.56–1.60 s |
The last two report the same allocated total (37,027,373,056 bytes). A single-threaded getattrlistbulk walk shows PRIVATESIZE alone adds ~0.5–0.6 s, while CLONEID is nearly free.
It's also redundant for the files this adjusts. ATTR_CMNEXT_CLONE_REFCNT counts full clones ("each shares all of its blocks with this file"), so once the refcount is above 1 the private size is 0. On real clones I got private = 0 for every file with a refcount of 2 or more. Requesting only ATTR_CMNEXT_CLONEID | ATTR_CMNEXT_CLONE_REFCNT and charging the full allocated size to the first member of each group (0 to the rest) gives the same result.
2. compareDirectoryEnumerationPerformance now fails on any tree that contains clones.
It asserts that .posix and .automatic totals match, which is no longer true by design: /Applications gives 37,173,268,480 vs 37,027,373,056 bytes, and a small clone fixture gives 11 MiB vs 7 MiB. Please leave allocated size out of that comparison (or compare it only when no clones are present). The test is opt-in (MACDIRSTAT_BENCHMARK_PATH), so CI won't catch this.
Minor: edited clones are still double-counted. A modified clone gets a new clone ID and a refcount of 1, so the blocks it still shares are counted in full. Worth adding to the Limits list.
Checked and fine: the packing order of the extended attributes; HFS+, exFAT and FAT volumes (the call succeeds and the clone fields come back as 0, so nothing changes); ATTR_CMNEXT_CLONE_REFCNT support in the macOS 15 kernel (xnu-11215); and swift build / swift test locally.
CLONE_REFCNT counts full clones, so a file with a refcount above one has no private blocks. Charging the whole allocated size to the first member of each clone group gives the same total without the ~0.5 s PRIVATESIZE costs on /Applications. The enumeration benchmark no longer compares allocated size, since the POSIX path can't see clones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the measurements! Done in dab8629:
|
|
Merged @horus-leonardo -- thank you for your contribution. |
st_blocksreports every block a clone shares with its siblings, so a file cloned N times added N full copies to the allocated size. On my Mac, a scan reported more used space than my 1 TB drive can hold; the excess came from cloned files in WhatsApp's container.getattrlistbulknow also requestsATTR_CMNEXT_CLONEIDandATTR_CMNEXT_CLONE_REFCNT(viaFSOPT_ATTR_CMN_EXTENDED). The refcount counts full clones, so a file whose count is above one has no private blocks: the first member of its clone group the scan sees carries the full allocated size, and the rest count 0. Logical size is unchanged and still reports every copy in full.Limits:
readdirfallback for filesystems withoutgetattrlistbulkgets no clone data, so nothing changes there.Validation
apfsClonesShareTheirBlocksOncetest: a 1 MiB file plus nine clones across three folders now counts one copy of allocated bytes, while logical size still counts all ten. Without the scanner change the test fails. It returns early when the temporary volume can't clone (not APFS).swift buildandswift testpass (17 tests).compareDirectoryEnumerationPerformanceno longer compares allocated size, since the POSIX path can't see clones. It passes on /Applications and on a small clone fixture.🤖 Generated with Claude Code