Skip to content

feat(scanner): sparse/clone-aware sizing with hard-link-deduped totals - #13

Merged
ohing504 merged 5 commits into
mainfrom
feat/a1-sparse-clone-aware-sizing
Jul 17, 2026
Merged

feat(scanner): sparse/clone-aware sizing with hard-link-deduped totals#13
ohing504 merged 5 commits into
mainfrom
feat/a1-sparse-clone-aware-sizing

Conversation

@ohing504

Copy link
Copy Markdown
Owner

What

Measure reclaimable space accurately for sparse files and hard-linked stores.

Changes

  • In-process sizing (scanner.Measure) replaces the per-artifact du fork, collecting disk (allocated blocks) and apparent (logical) size in one pass. Disk stays the primary figure for sorting, filtering and totals.
  • Sparse annotation: a Docker.raw image renders 8.6 GB (appears as 494.4 GB) — real disk size, then the size it reports. JSON gains an apparent_size field.
  • Hard-link dedup: total_size and the table total count blocks shared across artifacts (a pnpm store blob linked into node_modules) once, keyed by (dev, ino). Per-artifact sizes stay standalone; the table notes when it dedups.
  • Also: the scan spinner joins its goroutine on stop (no stale frame), and the Xcode device-support dedup key is scoped per platform.

Why in-process (reverses an earlier du choice)

Investigation flipped the premise: du -sk already reports disk usage, so size was already disk-based — what was missing is apparent. du can give neither apparent size nor cross-artifact inode data, so sizing moved in-process. Benchmarked at 1.15–1.48× du on real node_modules, and sizing is ~5% of a scan.

Not included

Clone-exact dedup (APFS code_sign_clone copies): distinct inodes, full blocks each, invisible to stat. Needs extent-level accounting — deferred.

Verification

  • Full tests + -race + lint pass; golden output unchanged.
  • Real Docker.raw smoke: renders 8.6 GB (appears as 494.4 GB), cross-checked against du/ls.
  • Hard-link smoke: two node_modules sharing a blob → total counts it once.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YSTf3ozF4SnboZHyEi7Ycc

ohing504 and others added 5 commits July 17, 2026 16:53
… key per platform

- Spinner.Stop now waits for the animation goroutine to exit before clearing
  the line, so a late frame can no longer reprint over the cleared line and
  leave a stale spinner artifact.
- enrichDeviceSupport keys builds by parent dir + "<model> <version>" instead
  of the bare name prefix, so version-only folders ("16.4 (build)") can never
  collide across DeviceSupport platforms if results are batched together.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSTf3ozF4SnboZHyEi7Ycc
Replace the per-artifact `du -sk` fork with an in-process walk (Measure)
that collects disk usage (allocated blocks, st_blocks×512 — matches du,
sparse-accurate) and apparent size (sum of logical file sizes) in one
pass. Disk stays primary for sorting/filtering/totals; apparent is new.

The table annotates materially sparse artifacts with their apparent size
(a Docker.raw image renders "24.0 GB (apparent 460.0 GB)") and JSON
carries apparent_size. Hard-linked inodes are counted once per artifact
and recorded (dev+ino keyed) so shared blocks can be netted across
artifacts in a follow-up.

In-process sizing benchmarks at 1.15-1.48x du on real node_modules
(~5% of a scan), a negligible cost for the added measurement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSTf3ozF4SnboZHyEi7Ycc
The grand total — JSON total_size and the table's Total line — now counts
blocks shared across artifacts via hard links once, through
model.DedupedTotal. A pnpm store blob also hard-linked into a project's
node_modules no longer inflates the total; per-artifact sizes stay
standalone, only the totals dedup, and the table notes when it did.

Keyed by (dev, ino) so identical inode numbers on different volumes
(external drives, container filesystems) are never falsely merged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSTf3ozF4SnboZHyEi7Ycc
"apparent" read as jargon and didn't convey that the file only nominally
occupies the larger size. The table now renders a sparse artifact as
"24.0 GB (appears as 460.0 GB)" — real disk first, the size it presents
as second. The JSON field stays apparent_size (the standard du term) for
tooling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSTf3ozF4SnboZHyEi7Ycc
Reconcile the CHANGELOG sizing entry — the per-artifact `du` fork is gone,
replaced by in-process Measure — and add entries for the apparent_size
field, the sparse "(appears as …)" annotation, and the (dev,ino)-deduped
total_size. Document the previously-undocumented --json output schema in
commands.md. Note accurate sizing in the README feature list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YSTf3ozF4SnboZHyEi7Ycc
@ohing504
ohing504 merged commit b39966c into main Jul 17, 2026
3 checks passed
@ohing504
ohing504 deleted the feat/a1-sparse-clone-aware-sizing branch July 17, 2026 11:12
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.

1 participant