Skip to content

fix(efficiency): count add-then-delete bytes as waste - #100

Merged
deveshctl merged 2 commits into
mainfrom
fix/efficiency-add-then-delete-waste
Aug 8, 2026
Merged

fix(efficiency): count add-then-delete bytes as waste#100
deveshctl merged 2 commits into
mainfrom
fix/efficiency-add-then-delete-waste

Conversation

@deveshctl

Copy link
Copy Markdown
Owner

Summary

Efficiency analysis now counts a file that is added in one layer and deleted in a later layer as wasted space. Previously such files contributed nothing to the waste total and never appeared in the wasted-files list, which understated waste and overstated the efficiency score for one of the most common image-bloat patterns.

Why

Container image layers are immutable and additive. When a later layer "deletes" a file, it only records a whiteout marker — the original bytes remain physically stored in the earlier layer and are transferred on every pull. They are never reclaimed. Treating a deleted file as "cleaned up" is correct for a live filesystem but wrong for a layered image.

This is the textbook "download a big archive, build, then delete it" pattern: the delete makes it look clean, but the bytes ship forever. Because the CI evaluator reads the same numbers, an image over its real byte budget could pass the lowest-efficiency and highest-wasted-bytes gates.

What changed

  • Waste accounting (image/efficiency.go): a path's occurrences are grouped into runs separated by deletions. A run that ends in a deletion now charges all of its occurrences as waste — nothing survives into the final image, yet every copy shipped. A run still live at the top of the stack keeps its last occurrence as before (that copy is the one present in the image). This handles add then delete then re-add correctly: the first run is fully charged, the re-install is a fresh live run and is not penalised.
  • Tests: added cases for add-then-delete, add-modify-delete, opaque-whiteout, and regression guards for same-run duplicates and still-live single files.
  • Docs: JSON export, CI integration, and configuration docs now describe the broadened wastedBytes definition and note that a score may be lower than a tool that counts only files duplicated across layers.

Compatibility

The score formula (1 - wastedBytes / (liveBytes + wastedBytes)), CI rules, thresholds, exit codes, and JSON schema are all unchanged. Affected images simply report an honest, higher waste figure and a correspondingly lower score.

Verification

Verified against a purpose-built image (alpine base, write a 1 MiB file, delete it, write and overwrite a 512 KiB file) exported both with and without BuildKit, and cross-checked byte-for-byte against the raw layer tars:

wastedBytes score deleted file listed
before 524,288 0.941 no
after 1,572,864 0.841 yes

The corrected total (1,048,576 + 524,288) matches the physical bytes stored in the layers.

A file added in one layer and deleted (whiteout or opaque whiteout) in a
later layer was contributing zero to WastedBytes and never appeared in
WastedFiles. Deleting a file in a later layer only records a whiteout; it
never reclaims the earlier layer's bytes, which remain stored in the image
and are transferred on every pull. Treating those bytes as "cleaned up" is
correct for a live filesystem but wrong for an immutable layered image, and
it let the classic download-build-rm bloat pattern pass the efficiency and
wasted-bytes gates undetected.

pathRuns now records why each run ended: a run closed by a deletion charges
all of its occurrences as waste (nothing survives, everything shipped), while
a run still live at the top of the stack keeps its last occurrence as before.
The score formula, CI rules, thresholds, exit codes, and JSON shape are
unchanged; affected images simply report an honest, higher waste figure.
occurrenceCount was accumulated over all runs unconditionally, so a
reinstalled file's live single-occurrence run inflated LayerCount even
though that copy is the surviving keeper and contributes no waste.

Fix: only accumulate occurrenceCount for runs that actually contribute
waste — the single-occurrence live run continue now skips the count
loop too, so the reinstall copy is not counted.

Also removes the stale pre-Round-8 comment paragraph from
TestEfficiency_InstallCleanReinstall_FirstCopyWasted that contradicted
the test's own assertion, and adds a LayerCount assertion to pin the
corrected behaviour.
@deveshctl
deveshctl merged commit fd660f1 into main Aug 8, 2026
13 checks passed
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