mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-27 18:57:09 +00:00
fix(resources): use daemon-reported reclaimable image bytes (#1154)
* fix(resources): use daemon-reported reclaimable image bytes The Reclaim banner summed per-image `VirtualSize` (or `Size`) across every image with no running container. That counts shared base layers once per image, so an unused base layer of 1 GB shared across ten builds showed up as 10 GB of "prunable" space. The actual prune frees the layer once and reports a much smaller `SpaceReclaimed`, leaving the banner and the post-prune toast badly out of step. Prefer Docker's own `ImageUsage.Reclaimable` (API v1.44+), which is the exact value `docker system df` displays. Older daemons fall back to the Docker CLI's internal formula: `LayersSize - sum(Size - SharedSize)` for in-use images, clamped to 0, skipping any image Docker flags with the -1 unknown-size sentinel. Verified against a live daemon: the banner now matches `docker system df`'s IMAGES RECLAIMABLE byte-for-byte. * fix(resources): treat SharedSize=-1 as 0 in fallback, don't drop in-use bytes The fallback formula is `LayersSize - used`. The previous version skipped any active image whose VirtualSize or SharedSize was -1 (Docker's "unknown" sentinel). Skipping leaves the image's bytes out of `used`, which reads back as reclaimable -- the exact inflation the PR set out to fix, just on older daemons. Treat SharedSize=-1 (or absent) as 0 so the image's full size counts as in-use, and only skip when no usable size is available at all (VirtualSize and Size both unknown). Under-reporting reclaimable is the safe direction; over-reporting was the original bug. Add a test for the SharedSize=-1 case with Size known, and rename the existing test so it reflects what is actually being asserted now. Addresses Codex audit blocker on PR #1154.
This commit is contained in:
@@ -145,16 +145,41 @@ class DockerController {
|
||||
return { bytes, count: reclaimable.length };
|
||||
};
|
||||
|
||||
const reclaimableImages = (items: any[]) => {
|
||||
// Prefer the daemon's own ImageUsage.Reclaimable (Docker API v1.44+); it is
|
||||
// the exact number `docker system df` displays and accounts for shared
|
||||
// layers correctly. On older daemons, fall back to LayersSize minus the
|
||||
// unique-to-in-use bytes, the same arithmetic the CLI uses internally.
|
||||
// Summing per-image Size/VirtualSize would over-report by the multiplicity
|
||||
// of every shared layer (the old bug that inflated the banner).
|
||||
const reclaimableImages = (items: any[], layersSize: number, serverReclaimable: number | undefined) => {
|
||||
if (!items || !Array.isArray(items)) return { bytes: 0, count: 0 };
|
||||
const reclaimable = items.filter(i => i.Containers === 0);
|
||||
const bytes = reclaimable.reduce((acc, item) => {
|
||||
let size = item.VirtualSize || item.Size || item.SharedSize || 0;
|
||||
if (item.UsageData && typeof item.UsageData.Size === 'number') {
|
||||
size = item.UsageData.Size;
|
||||
const reclaimable = items.filter(i => (i?.Containers ?? 0) === 0);
|
||||
if (serverReclaimable !== undefined && serverReclaimable >= 0) {
|
||||
return { bytes: serverReclaimable, count: reclaimable.length };
|
||||
}
|
||||
let used = 0;
|
||||
for (const item of items) {
|
||||
if (!item || (item.Containers ?? 0) <= 0) continue;
|
||||
// Choose the best non-negative size: prefer VirtualSize, fall back to
|
||||
// Size. If neither is known the image is truly unaccountable; skipping
|
||||
// it leaks at most one image's worth of bytes into the reclaim total.
|
||||
let virt = -1;
|
||||
if (typeof item.VirtualSize === 'number' && item.VirtualSize >= 0) {
|
||||
virt = item.VirtualSize;
|
||||
} else if (typeof item.Size === 'number' && item.Size >= 0) {
|
||||
virt = item.Size;
|
||||
}
|
||||
return acc + size;
|
||||
}, 0);
|
||||
if (virt < 0) continue;
|
||||
// SharedSize === -1 (or absent) means "unknown" on older daemons. Treat
|
||||
// it as 0 so the image's full size counts as in-use. Under-reporting
|
||||
// reclaimable is the safer direction; the previous skip-on-(-1) path
|
||||
// moved those bytes into the reclaimable total and re-inflated it.
|
||||
const shared = typeof item.SharedSize === 'number' && item.SharedSize >= 0
|
||||
? item.SharedSize
|
||||
: 0;
|
||||
used += Math.max(0, virt - shared);
|
||||
}
|
||||
const bytes = Math.max(0, (layersSize || 0) - used);
|
||||
return { bytes, count: reclaimable.length };
|
||||
};
|
||||
|
||||
@@ -175,7 +200,10 @@ class DockerController {
|
||||
return { bytes, count: reclaimable.length };
|
||||
};
|
||||
|
||||
const images = df.Images ? reclaimableImages(df.Images) : { bytes: 0, count: 0 };
|
||||
const imageUsage = (df as { ImageUsage?: { Reclaimable?: number } }).ImageUsage;
|
||||
const images = df.Images
|
||||
? reclaimableImages(df.Images, df.LayersSize ?? 0, imageUsage?.Reclaimable)
|
||||
: { bytes: 0, count: 0 };
|
||||
const containers = df.Containers ? reclaimableContainers(df.Containers) : { bytes: 0, count: 0 };
|
||||
const volumes = df.Volumes ? reclaimableVolumes(df.Volumes) : { bytes: 0, count: 0 };
|
||||
const buildCache = df.BuildCache ? reclaimableBuildCache(df.BuildCache) : { bytes: 0, count: 0 };
|
||||
|
||||
Reference in New Issue
Block a user