Repository navigation
feat: add --shared-blocks to count cloned and reflinked blocks once - #645
Draft
laurigates wants to merge 4 commits into
Draft
laurigates wants to merge 4 commits into
laurigates wants to merge 4 commits into
Conversation
Files that share physical blocks, such as APFS clones made with `cp -c` or `clonefile(2)`, were counted at full size each. uv and bun build per-project environments from clones of their global caches, so the totals overstate real disk use. --shared-blocks reads each file's clone id and extended flags with one getattrlist call (no open, works on unreadable files). Files flagged EF_SHARES_ALL_BLOCKS that have the same clone id belong to one clone family. clean_inodes, which already deduplicates hard links in a fixed order, now charges the family's blocks to its first member and gives later members a size of 0. The walk records clone ids in a map keyed by (inode, device), allocated only under the flag and filled only for clones, rather than in a field on every Node, so without the flag memory use is unchanged. A clone that has since been partly rewritten gets a new clone id and no EF_SHARES_ALL_BLOCKS, so it is still counted in full, as before. The flag needs macOS 14+ and is ignored, with a warning, on other platforms. It does not apply to apparent size, file counts or file times. Refs bootandy#590 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JkCD9AcGnt1yFN53gxksru Claude-Session: https://claude.ai/code/session_01JkCD9BwvWZwfV7FXbbuD2E
With this off-by-default cargo feature, --shared-blocks also handles clones that have been partly rewritten, which the clone id misses. Files the kernel flags EF_MAY_SHARE_BLOCKS are opened and mapped with fcntl(F_LOG2PHYS_EXT). Their physical extents feed a per-device map of byte ranges already counted, and each file is charged only for bytes no earlier file has claimed. Matching byte ranges rather than whole extents is what catches a partly rewritten clone: once either copy is written to, the two map the same blocks as differently split extents. Holes (device offset -1) are skipped. A file that can't be opened or fully mapped (compressed files return ENOTSUP) is counted in full. On ~/.cargo/registry (hyperfine, 10 runs) the feature build took 333.0 ms with --shared-blocks, against 316.4 ms for the default build and 258.4 ms for upstream master without the flag. The feature only adds code under cfg(feature = "exact-shared-blocks"), so dropping it means deleting that code, or reverting this commit. Refs bootandy#590 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JkCD9AcGnt1yFN53gxksru
btrfs and XFS reflinks (`cp --reflink`) share physical extents the same way APFS clones do, and were likewise counted once per file. Linux has no clone id, and no way to learn that a file may share blocks without opening it, so Linux uses the extent mode that the macOS exact-shared-blocks feature already has: FS_IOC_FIEMAP per file, and the byte-range map in clean_inodes. build.rs sets cfg(shared_extents) for Linux or the feature, which selects that mode in one place. - One statfs per device decides whether a filesystem can share extents at all (btrfs, XFS, OCFS2, bcachefs). Files elsewhere, such as ext4, tmpfs or NFS, are never opened. - Only extents flagged FIEMAP_EXTENT_SHARED are recorded, since others can't overlap another file. Inline, delayed-allocation and unknown extents have no real address, and btrfs compressed extents don't map byte for byte, so these are counted in full. - ZFS block clones can't be detected: ZFS has no FIEMAP. A new CI job loop-mounts btrfs and XFS images and runs an ignored integration test that reflinks a 1 MiB file twice and rewrites one block of the second copy. In a privileged ubuntu:24.04 container (kernel 7.2.12) it passed on both filesystems, and failed with `left: 0, right: 2093056` once the extent read was stubbed out. Refs bootandy#590 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JkCD9AcGnt1yFN53gxksru
…inux (#2) * perf: give rayon workers private fd tables under --shared-blocks on Linux --shared-blocks opens and closes every file to FIEMAP it. All threads of a process share one fd table, and open() and close() both take its spinlock (files->file_lock), so on a never-reflinked XFS tree the walk gets slower past about four threads. Each rayon worker now calls unshare(CLONE_FILES) at start, on Linux and only with --shared-blocks. That is safe only while every fd stays on the thread that opened it. get_shared_blocks opens and closes its file in one call. A DirEntry holds its directory stream, whose last drop calls closedir(), and DirEntry::file_type() falls back to a statx relative to the directory fd for DT_UNKNOWN, so walk_dir now resolves file types on the reading thread and hands workers borrowed entries; the entries drop there. Measured on an OrbStack VM (kernel 7.2.12, 13 vCPUs), 100,000 never-reflinked files, --shared-blocks -T 13, hyperfine, both orders: 76.2 -> 29.6 ms, system time 708 -> 134 ms. Output is byte-identical to the previous build at -T 1/4/13 on never-reflinked, reflinked and mixed trees. The default walk and reflinked trees are unchanged within noise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JkCD9CsC2rrmnMz6pAF8Vw * perf: drop DirEntry values on the workers again The previous commit kept each directory's DirEntry values on the reading thread so the directory stream's closedir() ran there. That also made the entries drop serially on that thread, and on bare metal (Ryzen 9 3900X, 24 threads, 300,000 files) the default walk got about 3% slower, reproducible with the builds in either order. ReadDir holds the same Arc as its entries, so keeping the ReadDir alive (collect via by_ref) until the parallel collect returns is enough: the workers drop their entries as before, the ReadDir is the last holder, and closedir() still runs on the thread that opened the directory. File types are still resolved on the reading thread first, for the DT_UNKNOWN statx fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JkCD9CsC2rrmnMz6pAF8Vw --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
laurigates
added a commit
to laurigates/claude-plugins
that referenced
this pull request
Nov 7, 2026
…PR base (#3453) * fix(git-plugin): ignore the pre-update head's checks in merge-chain.sh merge-chain.sh read statusCheckRollup straight after `gh pr update-branch` and stopped on the first failed check. GitHub reads can lag the update, so that first read may still show the old head and its rollup. On laurigates/mcu-tinkering-lab#945 the chain stopped CI_RED on a gitleaks run that failed on the pre-update head for a DNS error, while the same check passed on the post-update head. The script now records the head before each update. Unless gh printed "already up-to-date" (its BehindBy == 0 path, where the head stays put), wait_for_ci ignores every read whose head is still the pre-update one: no failure is counted, nothing settles, and the empty-rollup wait restarts. A head that never moves within --timeout fails closed as ERROR head_not_updated. test-merge-chain.sh gains a stub head lag and cases 317-319; 317 and 318 fail on the old script, and 319 keeps a no-op update's failed check CI_RED. The stub now gives a repeat update a new head, as a real update would. Fixes #3421 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JkCD9ADTrR7yJ2rNNTq82W * feat(git-plugin): catch a fork PR based on an open upstream PR's head Merging a fork PR whose base branch is also the head of an open PR against the upstream repository pushes the merged commits into that upstream PR. Squash-merging laurigates/dust#2 did exactly that to bootandy/dust#645, and nothing in git-merge-hazards or merge-chain.sh looked past the fork. git-merge-hazards §2 gains a skill-only pointer to references/fork-upstream-base.md, which carries the hand check (the repo's fork parent, then the parent's open PRs with head <owner>:<base>) and what to do when updating the upstream PR is intended. To stay inside the 10,000-character band, the Agentic Optimizations table drops its merge-over-red row, which restated §4's prose. sync-notes.md records both. merge-chain.sh reads the repository once per run (default branch, fork parent). For a PR in a fork whose base is not the default branch it lists the parent's open PRs for that head: a hit is SKIPPED base_is_upstream_pr_head, naming the upstream PR, and a failed read is ERROR. Outside a fork the check does not run, so a same-repo stacked base is unaffected. test-merge-chain.sh cases 120-125 cover the skip, a non-default base with no upstream PR, a default-branch base, a non-fork stack and both failed reads; 11 assertions fail on the previous script. Fixes #3445 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JkCD9ADTrR7yJ2rNNTq82W * fix(regression-ledger): move this PR's ledger rows into entry files The bot's union merge of origin/main kept the pre-#3469 ledger table and appended it to the new index page, re-adding the rows #3469 moved into docs/regression-ledger/. Restore the index from main and record this PR's own rows as docs/regression-ledger/<number>-<slug>.md entries, the format #3469 introduced. check-regression-ledger.sh reports STATUS=OK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JkCD9ADTrR7yJ2rNNTq82W --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This was referenced Nov 9, 2026
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.
Draft for #590, as discussed there. Adds an off-by-default
--shared-blocksflag that counts blocks shared between files (APFS clones, btrfs/XFS reflinks) once instead of once per file.The example from the issue, five
cp -cclones of a 200 MiB file:It's four commits; the second and the fourth can each be dropped on their own:
getattrlistper file reads the clone id andEF_SHARES_ALL_BLOCKS, without opening the file. Files with the same clone id are one family;clean_inodescharges the first one and gives the rest size 0, the same way it already handles hard links. A clone that has since been partly rewritten is still counted in full.exact-shared-blockscargo feature, off by default. Files flaggedEF_MAY_SHARE_BLOCKSget their extents mapped withF_LOG2PHYS_EXT, and each file is charged only for byte ranges no earlier file has claimed, so partly rewritten clones are handled too.statfsper device skips filesystems that can't share extents, so files on ext4, tmpfs or NFS are never opened. A new CI job runs a reflink test on btrfs and XFS loop images.open()andclose()take, so they queue on it. Each rayon worker now callsunshare(CLONE_FILES)at start, Linux and--shared-blocksonly. That is only safe while every fd stays on the thread that opened it, sowalk_dirresolves file types and drops theReadDiron the thread that read the directory. That rule is the extra complexity this commit adds to the walker.On your complexity concern: the walk change is a few lines in
process_entry(plus, with commit 4, keeping each directory'sReadDiron the thread that read it), and the rest is platform code inplatform.rsplus the extra accounting inclean_inodes. Without the flag none of it runs andNodeis unchanged; time and peak memory with the flag off match master on macOS and Linux.With the flag on (
hyperfine -N -w 2 -r 10, release builds; Linux rows on a GCP n2-standard-8 VM with kernel 7.0, before commit 4):--shared-blocks~/.cargo/registryCommit 4 on 100,000 never-reflinked files with
--shared-blocks, builds run in both orders:Output is identical to before at every thread count tested, and the default walk and reflinked trees are unchanged. Outside that VM the gain is 3–11%, so commit 4 is the first one I'd drop if the walker rule is unwelcome.
The reflinked XFS case doesn't scale with threads. The time goes into XFS itself: FIEMAP's shared-extent check takes a lock per allocation group, so the XFS range above is 32 vs 4 allocation groups.
cargo testpasses in both builds on macOS, and on Linux with commit 4. The reflink test from the new CI job passed on btrfs and XFS in a privileged ubuntu:24.04 container, but the job itself hasn't run in Actions yet.Happy to drop commit 2 or 4, or change anything else, if that makes it easier to take.
Closes #590
🤖 Generated with Claude Code
https://claude.ai/code/session_01JkCD9BwvWZwfV7FXbbuD2E