Fix dashboard image disk usage calculation (#1461)
* fix dashboard image disk usage calculation * preserve image size fallback behavior
This commit is contained in:
1 parent
2a9299f932
commit
560b21e03f
4 files changed
+95
-4
No files matched your search
@@ -0,0 +1,32 @@
|
||||
export interface DockerDiskUsageLike {
|
||||
LayersSize?: unknown;
|
||||
ImageUsage?: {
|
||||
TotalSize?: unknown;
|
||||
} | null;
|
||||
Images?: Array<{
|
||||
Size?: unknown;
|
||||
}> | null;
|
||||
}
|
||||
|
||||
function asNonNegativeFiniteNumber(value: unknown): number | null {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Docker's aggregate image disk usage without double-counting shared layers.
|
||||
*
|
||||
* `Images[].Size` is each image's virtual size (shared + unique), so adding those
|
||||
* values overstates disk usage whenever images share layers. Docker exposes the
|
||||
* deduplicated total as `ImageUsage.TotalSize` in the current API and as
|
||||
* `LayersSize` in the legacy response shape.
|
||||
*/
|
||||
export function getImageDiskUsageTotalSize(
|
||||
diskUsage: DockerDiskUsageLike | null | undefined
|
||||
): number | null {
|
||||
const currentTotal = asNonNegativeFiniteNumber(diskUsage?.ImageUsage?.TotalSize);
|
||||
if (currentTotal !== null) return currentTotal;
|
||||
|
||||
return asNonNegativeFiniteNumber(diskUsage?.LayersSize);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '$lib/server/docker';
|
||||
import { listComposeStacks } from '$lib/server/stacks';
|
||||
import { countLivePending } from '$lib/server/pending-updates-core';
|
||||
import { getImageDiskUsageTotalSize } from '$lib/server/docker-disk-usage-core';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { parseLabels } from '$lib/utils/label-colors';
|
||||
|
||||
@@ -239,9 +240,11 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
|
||||
|
||||
// Process disk usage from /system/df for accurate size data
|
||||
if (diskUsage) {
|
||||
// Images: use Size from /system/df
|
||||
// Images: use Docker's aggregate size so shared layers are counted once
|
||||
envStats.images.total = diskUsage.Images?.length || images.length;
|
||||
envStats.images.totalSize = diskUsage.Images?.reduce((sum: number, img: any) => sum + getValidSize(img.Size), 0) || 0;
|
||||
envStats.images.totalSize = getImageDiskUsageTotalSize(diskUsage)
|
||||
?? diskUsage.Images?.reduce((sum: number, img: any) => sum + getValidSize(img.Size), 0)
|
||||
?? 0;
|
||||
|
||||
// Volumes: use UsageData.Size from /system/df
|
||||
envStats.volumes.total = diskUsage.Volumes?.length || volumes.length;
|
||||
|
||||
@@ -26,6 +26,7 @@ import { prefersJSON, sseToJSON } from '$lib/server/sse';
|
||||
import type { EnvironmentStats } from '../+server';
|
||||
import { parseLabels } from '$lib/utils/label-colors';
|
||||
import { isEdgeConnected } from '$lib/server/hawser';
|
||||
import { getImageDiskUsageTotalSize } from '$lib/server/docker-disk-usage-core';
|
||||
|
||||
|
||||
// Skip disk usage collection (Synology NAS performance fix)
|
||||
@@ -434,9 +435,11 @@ async function getEnvironmentStatsProgressive(
|
||||
: getCachedDiskUsage(env.id)
|
||||
.then((diskUsage) => {
|
||||
if (diskUsage) {
|
||||
// Update images with disk usage data (more accurate)
|
||||
// Update images with Docker's deduplicated aggregate size
|
||||
envStats.images.total = diskUsage.Images?.length || envStats.images.total;
|
||||
envStats.images.totalSize = diskUsage.Images?.reduce((sum: number, img: any) => sum + getValidSize(img.Size), 0) || envStats.images.totalSize;
|
||||
envStats.images.totalSize = getImageDiskUsageTotalSize(diskUsage)
|
||||
?? diskUsage.Images?.reduce((sum: number, img: any) => sum + getValidSize(img.Size), 0)
|
||||
?? envStats.images.totalSize;
|
||||
|
||||
// Volumes from disk usage
|
||||
envStats.volumes.total = diskUsage.Volumes?.length || 0;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// @ts-expect-error -- bun:test is a runtime built-in with no types installed
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { getImageDiskUsageTotalSize } from '../src/lib/server/docker-disk-usage-core';
|
||||
|
||||
describe('getImageDiskUsageTotalSize', () => {
|
||||
test('uses Docker aggregate size instead of summing virtual image sizes', () => {
|
||||
const diskUsage = {
|
||||
LayersSize: 70_326_328_267,
|
||||
Images: [
|
||||
{ Size: 189_295_958_800 },
|
||||
{ Size: 82_498_083_216 }
|
||||
]
|
||||
};
|
||||
|
||||
expect(getImageDiskUsageTotalSize(diskUsage)).toBe(70_326_328_267);
|
||||
});
|
||||
|
||||
test('prefers the current ImageUsage.TotalSize response field', () => {
|
||||
expect(getImageDiskUsageTotalSize({
|
||||
ImageUsage: { TotalSize: 42 },
|
||||
LayersSize: 99
|
||||
})).toBe(42);
|
||||
});
|
||||
|
||||
test('supports the legacy LayersSize response field', () => {
|
||||
expect(getImageDiskUsageTotalSize({ LayersSize: 42 })).toBe(42);
|
||||
});
|
||||
|
||||
test('preserves zero as a valid aggregate size', () => {
|
||||
expect(getImageDiskUsageTotalSize({
|
||||
ImageUsage: { TotalSize: 0 },
|
||||
LayersSize: 99
|
||||
})).toBe(0);
|
||||
expect(getImageDiskUsageTotalSize({ LayersSize: 0 })).toBe(0);
|
||||
});
|
||||
|
||||
test('falls back from an invalid current value to a valid legacy value', () => {
|
||||
expect(getImageDiskUsageTotalSize({
|
||||
ImageUsage: { TotalSize: Number.NaN },
|
||||
LayersSize: 42
|
||||
})).toBe(42);
|
||||
});
|
||||
|
||||
test('returns null when no trustworthy aggregate is available', () => {
|
||||
expect(getImageDiskUsageTotalSize(undefined)).toBeNull();
|
||||
expect(getImageDiskUsageTotalSize({})).toBeNull();
|
||||
expect(getImageDiskUsageTotalSize({ LayersSize: -1 })).toBeNull();
|
||||
expect(getImageDiskUsageTotalSize({
|
||||
ImageUsage: { TotalSize: '42' },
|
||||
LayersSize: null
|
||||
})).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user