From af89423f66b6cf7543bfbaa7429c275ca5f1342a Mon Sep 17 00:00:00 2001 From: Junker der Provinz <133605895+junkerderprovinz@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:54:52 +0200 Subject: [PATCH] fix(docker): report current CPU usage instead of the lifetime average (#6249) --- packages/request-handler/src/docker.ts | 21 +++++++++++++++---- .../request-handler/src/test/docker.spec.ts | 20 ++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/request-handler/src/docker.ts b/packages/request-handler/src/docker.ts index 12a78c05a..a238b4a3c 100644 --- a/packages/request-handler/src/docker.ts +++ b/packages/request-handler/src/docker.ts @@ -300,7 +300,10 @@ async function getContainersWithStatsAsync() { const stats = await instance .getContainer(container.Id) - .stats({ stream: false, "one-shot": true }) + // Not in one-shot mode: the daemon then includes precpu_stats (the + // previous reading), which is required to compute the current CPU usage + // as a delta. One-shot omits it and would only expose lifetime totals. + .stats({ stream: false }) .catch( () => ({ @@ -336,12 +339,22 @@ export function calculateCpuUsage(stats: ContainerStats): number { } const numberOfCpus = stats.cpu_stats.online_cpus; - const usage = stats.cpu_stats.system_cpu_usage; - if (!usage || usage === 0) { + + // Docker's own formula for the current (instantaneous) CPU usage: it compares + // the latest reading against the previous one (precpu_stats). Dividing the + // cumulative total_usage by the cumulative system_cpu_usage instead would + // report the container's lifetime average, which stays high long after a load + // spike has ended. precpu_stats is only present when the stats are NOT fetched + // in one-shot mode; when it is absent (e.g. Podman) the deltas fall back to the + // cumulative totals, matching the previous behaviour. + const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - (stats.precpu_stats?.cpu_usage?.total_usage ?? 0); + const systemDelta = (stats.cpu_stats.system_cpu_usage ?? 0) - (stats.precpu_stats?.system_cpu_usage ?? 0); + + if (systemDelta <= 0 || cpuDelta < 0) { return 0; } - return (stats.cpu_stats.cpu_usage.total_usage / usage) * numberOfCpus * 100; + return (cpuDelta / systemDelta) * numberOfCpus * 100; } export function calculateMemoryUsage(stats: ContainerStats): number { diff --git a/packages/request-handler/src/test/docker.spec.ts b/packages/request-handler/src/test/docker.spec.ts index b4e34f816..12dbb347b 100644 --- a/packages/request-handler/src/test/docker.spec.ts +++ b/packages/request-handler/src/test/docker.spec.ts @@ -75,6 +75,26 @@ describe("calculateCpuUsage", () => { // (500 / 100000) * 2 * 100 = 1 expect(calculateCpuUsage(stats)).toBe(1); }); + + test("should use the delta against precpu_stats for the current usage", () => { + const stats = createStats({ + cpu_stats: { online_cpus: 4, cpu_usage: { total_usage: 1_002_000 }, system_cpu_usage: 20_000 }, + precpu_stats: { cpu_usage: { total_usage: 1_000_000 }, system_cpu_usage: 10_000 }, + }); + // Despite a large lifetime total_usage, only the recent delta counts: + // (2000 / 10000) * 4 * 100 = 80 + expect(calculateCpuUsage(stats)).toBe(80); + }); + + test("should report 0 for a container that was busy over its lifetime but is idle now", () => { + const stats = createStats({ + cpu_stats: { online_cpus: 4, cpu_usage: { total_usage: 1_000_000 }, system_cpu_usage: 20_000 }, + precpu_stats: { cpu_usage: { total_usage: 1_000_000 }, system_cpu_usage: 10_000 }, + }); + // No CPU delta since the last reading -> current usage is 0, even though the + // lifetime average would be high. This is the regression the fix addresses. + expect(calculateCpuUsage(stats)).toBe(0); + }); }); describe("calculateMemoryUsage", () => {