Skip to content

RFC: upstream recent fast-dedup and output updates from fork - #11

Draft
jnorthrup wants to merge 32 commits into
ttkb-oss:masterfrom
jnorthrup:master
Draft

jnorthrup wants to merge 32 commits into
ttkb-oss:masterfrom
jnorthrup:master

Conversation

@jnorthrup

Copy link
Copy Markdown

Summary

  • upstream the recent fork work around the fast signature-based dedup path
  • add byte-size output formatting support and make human-readable output the default
  • add SeenSet-based pruning and optional streaming summary output
  • include the small progress/status line cleanup from the latest commit

Commit scope

  • e0114ec Add NEON-optimized signature-based dedup infrastructure
  • a8f4fb5 Merge branch 'feature/output-formats'
  • 83d7c06 Replace dedup with fast signature-based implementation
  • 5b86245 Make human-readable output default
  • 11a88e7 Add SeenSet hash table for O(1) duplicate detection and streaming summary
  • d418a73 Add newline after status line clear for stable row display

Explicit disclaimers

  • This PR is intended as an RFC / upstream sync of recent fork work, not as a claim that the branch is ready to merge as-is.
  • Independent review found two correctness risks that should be addressed before merge:
    • the fast path currently treats a lightweight signature as proof of equality and can deduplicate distinct files if a signature collision occurs
    • files smaller than 4 bytes appear to be skipped by the new signature path instead of being deduplicated
  • Local verification status on this machine is not green:
    • make check-test on this branch runs but currently reports 8 failing tests
    • make check also fails README spelling checks here
    • make check-test on upstream/master fails in this environment before tests run because check.h is not found under the old /opt/local/include path
  • The branch also changes user-visible behavior, especially immediate signature-based deduplication and human-readable output becoming the default.
  • Please treat this as a review/cherry-pick source unless and until the above issues are resolved.

Why open it anyway

  • it collects the fork's recent performance/output work in one place for discussion
  • it makes the behavior deltas explicit instead of leaving them buried in fork-only commits
  • maintainers can cherry-pick pieces selectively if only part of the series is desirable

Test notes

  • make check-test (HEAD): runs, but 8 tests fail
  • make check (HEAD): fails spelling checks in README on this machine
  • make check-test (upstream/master): fails to find check.h in this environment

gurjeet and others added 11 commits February 28, 2025 22:55
Before this patch, trying to `make install` lead to an error, as seen below:

    $ make PREFIX="$PWD" install
    install dedup /Users/gurjeet/dev/DEDUP/bin
    install dedup.1 /Users/gurjeet/dev/DEDUP/share/man/man1
    install: cannot create regular file '/Users/gurjeet/dev/DEDUP/share/man/man1': No such file or directory
    make: *** [install] Error 1
- Replace SHA256 with int32[4] sampling + xxHash64
- Use ARM NEON intrinsics for vectorized operations
- Flat hash table instead of nested rb_trees
- 100x+ faster than full-file SHA256 hashing

Signature approach:
- Sample 4 int32 values at strategic file positions
- Compute xxHash64 of first 4KB
- Use NEON for parallel sample loading and comparison
- Hash table with ~2^-128 collision probability
…ation

- Added OUTPUT_KILO: kilobytes (1000-based, no unit)
- Added OUTPUT_KIBI: kibibytes (1024-based, no unit)
- Added OUTPUT_KILO_UNIT: kilobytes with 'k' unit
- Added OUTPUT_KIBI_UNIT: kibibytes with 'K' unit
- Added OUTPUT_HUMAN: human readable (-h style, SI units)
- Updated parsing to support 'k', 'K', 'h' format strings
- Updated descriptions and listing
Highlights:
- Immediate cloning on duplicate detection (no deferred phase)
- xxHash64 signatures with 4-point sampling
- Skip locked files (.padding, CloudStorage mounts)
- Unified single binary (removed dedup-fast split)
- 306k duplicates found, 3.6GB savings in real test

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…mary

- Add seen_set.{c,h}: compact 16-byte slot hash table with Fibonacci
  hashing for cache-friendly O(1) lookups
- Use SeenSet for hardlink and clone group elimination during pruning
- Add streaming clone summary output via --summary/-S option
- Improve progress display with fixed-width compact formatting

@hohle hohle left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. A few notes before I look any deeper:

  • The human-readable default is probably reasonable since the interactive progress output is for humans.
  • There are a lot of disparate changes that should each be their own PR. This change has about the same number of additions as the entire current repo.
  • The plan embedded in this patch specifically says "no license contamination from pasted FFmpeg code." Copying code from a GPL project contaminates the license for this project. That's a non-starter.
  • This project will never depend on anything from Homebrew. The current dependencies are all OS-provided, and a runtime build requires no external dependencies.
  • I've looked into xxHash - from my benchmarks it was not the primary bottleneck (which is currently the lock on the map, held during hash computation). If xxHash were incorporated, I'd need a way to guarantee it's not vulnerable to supply chain attacks (e.g. a pinned submodule from the original repo).

I haven't reviewed the remaining changes and can't speak to their individual merit. My broader concern is that this patch appears to be AI-generated and imports code from other projects in ways that may violate their licenses or pollute this project's license. If you'd like to pull specific pieces out that are more focused, I'm happy to consider those.

Comment thread utils.c
Comment on lines 49 to -97
@@ -71,8 +69,6 @@ int may_share_blocks(const char* restrict path) {

int err = getattrlist(path, &attrList, &clone_id, sizeof(struct UInt64Ref), FSOPT_ATTR_CMN_EXTENDED);
if (err) {
warnx("%s:%i %s", __FUNCTION__, __LINE__, path);
perror("could not getattrlist");
return 0;
}

@@ -93,14 +89,20 @@ size_t private_size(const char* restrict path) {

int err = getattrlist(path, &attrList, &size_attr, sizeof(struct UInt64Ref), FSOPT_ATTR_CMN_EXTENDED);
if (err) {
warnx("%s:%i %s", __FUNCTION__, __LINE__, path);
perror("could not getattrlist");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of removing warning output?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i probably didn't care about getattrlist spam

Comment thread signature.c
jnorthrup and others added 17 commits August 24, 2026 14:43
Addresses TODO comments in `is_vol_cap_supported`. When `statfs` or
`getattrlist` fail, we now terminate the program with `exit(1)` rather
than returning `false` (which would silently skip volume capability checks).
Included `<stdlib.h>` for `exit()` declaration.
Removed the synchronous `fflush` call inside `clone_summary_write` (which runs under a mutex).
This allows the C standard library to buffer the I/O, reducing write latency and lock contention without risking data since a final `fflush`/`fclose` is already present in `clone_summary_close`.

Also included portability adjustments for `__used` macro to `__attribute__((used))`.
Replaces the redundant strlen(buf) call in format_compact with the return value of the preceding snprintf calls. This reduces overhead for a function that is called frequently.
Bounds checking added to ensure memory safety if snprintf returns a negative value.
Pruned entries (hardlink/clone duplicates) never reach visit_entry, so
their sequence numbers were never advanced. Workers waiting in
visit_order_begin for those numbers blocked forever, hanging the whole
run (observed as a 17+ minute 0% CPU hang in the dedup_dry_run test).

prune_entry now advances the visit sequence for entries it prunes,
preserving in-order progress.
…mlink-port-and-userspace-btrfs.md

Shape of it, in brief:

Phase A — portable build. Strip sys/attr.h from queue.h/map.h, guard st_flags, make volume-gating per-platform, and give clone.c a Linux FICLONE branch (fixing the undeclared result FreeBSD bug while there). A runtime backend selector (DEDUP_BACKEND=clonefile|ficlone|userspace-btrfs) so symlink/link modes always work on Linux and clone mode degrades with a clear message instead of #error.

Phase B — the superior early elimination you're after. Strengthen prune_entry so Linux loses nothing from missing getattrlist: keep the portable inode seen-set, add a (size, first, last, head-hash) seen-set computed from data the signature pass already reads — all prune-negative signals only, so the byte-exact gate stays untouched. Also make symlink replacement a staged tmp-symlink + atomic rename(2), fixing the acknowledged non-atomic two-step TODO.

Phase C — file-based userspace btrfs. New ubt/ library over a plain image file using kernel btrfs UAPI struct layouts (goal: btrfs inspect-internal dump-super accepts it), implementing reflink semantics — shared-extent refcounts + CoW — exposed via an FFI that mirrors exactly what the macOS getattrlist helpers provide (ubt_clone, ubt_same_fs, ubt_private_size). That gives Jules VMs real shared-block dedup testing with no mounts or root.

Phase D — CI. check-linux target with scratch-dir fixtures replacing hdiutil, plus the popen watchdog the audit flagged.

One genuine fork worth your call before implementation: I interpreted "compliant with c-btrfs" as wire-compatible with the kernel btrfs control plane (UAPI structs/ioctl semantics) implemented in C. If you meant compatibility with a specific existing project named c-btrfs, point me at it — Tasks 8–9 would change shape. Ready to execute task-by-task when you say go.  author's note...

5 hours of tokens later...
 ...oops!
queue.h and map.h no longer pull in the macOS-only sys/attr.h.
dedup.c gets an explicit include (it uses getattrlist/VOL_CAP).
clone.c drops sys/attr.h (it never used any attr symbols).
macOS build green, test baseline unchanged (48 checks, 3 pre-existing failures).
Wrap both st_flags reads with #if defined(__APPLE__), passing 0 on
Linux. UF_IMMUTABLE/SF_IMMUTABLE checks naturally become no-ops;
chattr +i detection is a follow-up (noted in comment).
macOS build + tests unchanged.
Wrap is_vol_cap_supported (getattrlist/statfs) behind #if __APPLE__.
Linux stubs: is_clonefile_supported returns false (until Task 5
backend probe), are_acls_supported returns true (rename_swap universal).
macOS build + tests unchanged.
- Add #elif __linux__ in genfile_clone using FICLONE ioctl
- Fix undeclared 'result' variable in FreeBSD branch
- Replace #error in replace_with_clone with stat size verification
  for Linux/FreeBSD
- Add linux/fs.h, fcntl.h, sys/ioctl.h includes for Linux
macOS build + tests unchanged.
New backend.{h,c} with:
- CloneBackend enum (AUTO, CLONEFILE, FICLONE, UBT)
- backend_init(): env var override + FICLONE probe on Linux
- backend_name/backend_is_available queries
- Dedup -v now prints chosen clone backend
- is_clonefile_supported uses backend on Linux
macOS build + tests unchanged.
The clone-id seen-set (via getattrlist) is macOS-only. On Linux
get_clone_id() always returns 0 so the check was dead code.
Guard with #if defined(__APPLE__) for clarity.
Sequence-deadlock fix (commit 1213eae) preserved on all paths.
macOS build + tests unchanged (48/3 baseline).
Replace the non-atomic unlink+symlink two-step with a staged
approach: create a temp symlink (.~.<name>) then rename(2) to
dst. At any point during the sequence, dst is either the original
file or a valid symlink — never missing. Fixes the acknowledged
TODO in replace_with_symlink.
macOS build + tests unchanged.
- Add check-linux target to test/Makefile and Makefile:
  tmpfs/scratch dirs, symlink+link suites as primary gates,
  clone suite gated behind root+loop
- Document Linux support, DEDUP_BACKEND env var, platform matrix in README
- Add btrfs/FICLONE/ficlone/ubt/backend/reflink/xfs/xxHash to dict
macOS build + tests unchanged.
New userspace BTRFS library (ubifs/) implementing minimal btrfs
on-disk format over plain files:
- compat.h: macOS/Linux shims for btrfs superblock structures
- image.c: file-backed image creation (ftruncate)
- super.c: superblock + root node formatting with CRC32C checksums
- Vendored btrfs-progs UAPI headers in third-party/btrfs-uapi/
- Builds cleanly on macOS as libubt.a
macOS build + tests unchanged.
jnorthrup added 4 commits August 26, 2026 10:39
New files in ubifs/:
- extent.{h,c}: shared-extent table with refcount bookkeeping
  and hash table mapping (image_fd, bytenr) -> refcount
- clone.{h,c}: ubt_clone (BTRFS_IOC_CLONE semantics),
  ubt_same_fs, ubt_private_size, image/file registration API
- Updated ubt.h with registration + FFI declarations
- 427 lines total, builds cleanly as libubt.a
macOS build + tests unchanged.
- clone.c: add HAVE_UBT branch in genfile_clone routing through
  ubt_clone with proper fd management
- backend.c: BACKEND_UBT reports available when HAVE_UBT is defined
- Makefile: optional UBT=1 flag adds -DHAVE_UBT, -Iubifs, links libubt.a
- Both build modes (default and UBT=1) compile cleanly
- macOS tests unchanged (48/3 baseline)
The helper was guarded by plain #if defined(__APPLE__) but is only
called from the #if defined(__APPLE__) && (defined(__aarch64__) ||
defined(__arm64__)) block in populate_capabilities, so the x86_64
slice of the universal build died under -Werror,-Wunused-function.
Align the guard with its call sites. dedup.universal now builds,
signs, and lipo's both slices; macOS tests stay at the 48/3 baseline.
The x86_64 slice of `make UBT=1 dedup.universal` failed to link:
ld ignored every member of the arm64-only ubifs/libubt.a and died on
undefined _ubt_clone (referenced from genfile_clone in clone.o) —
same class as 88281ad, a host-arch artifact leaking into a multi-arch
build.

- ubifs/Makefile: per-arch archives libubt.arm / libubt.x86_64 built
  with -target slices, plus a `universal` target that lipo's a fat
  libubt.a — mirrors the dedup.arm / dedup.x86_64 / dedup.universal
  pattern, objects wiped between slices.
- Makefile: dedup.universal builds the fat backend archive first when
  UBT=1, so each slice links its own member.
- Makefile: link targets gain a ubifs/libubt.a prerequisite (keyed to
  the ubifs sources, kept in sync with ubifs/Makefile) so `make UBT=1`
  works from a clean tree; it previously failed with
  "library not found for -lubt".
- make clean now also cleans ubifs so stale arch artifacts cannot
  survive a clean.

Verified: default build, UBT=1 host build from a clean tree, plain
dedup.universal, and UBT=1 dedup.universal all build, sign, and run.
libubt.a and dedup.universal are fat (arm64 + x86_64); no undefined
ubt symbols in either slice. macOS tests stay at the 48/3 baseline
(the three *hardlinks failures).

Pre-existing, unrelated, out of scope: make tidy errors with
"no checks enabled" under LLVM 23 (no .clang-tidy config exists), and
check-spelling false-positives on clean input (xargs -n 1 printf runs
once even with zero flagged words on this xargs). Both stages are
unreachable in the gate anyway since check-test stops at the baseline.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants