Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 11 additions & 23 deletions environment/docker/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func (e *Environment) pollResources(ctx context.Context) error {
Uptime: uptime,
Memory: calculateDockerMemory(v.MemoryStats),
MemoryLimit: v.MemoryStats.Limit,
CpuAbsolute: calculateDockerAbsoluteCpu(v.PreCPUStats, v.CPUStats),
CpuAbsolute: calculateDockerAbsoluteCpu(v),
Network: environment.NetworkStats{},
}

Expand Down Expand Up @@ -130,28 +130,16 @@ func calculateDockerMemory(stats container.MemoryStats) uint64 {
// Calculates the absolute CPU usage used by the server process on the system, not constrained
// by the defined CPU limits on the container.
//
// @see https://github.com/docker/cli/blob/aa097cf1aa19099da70930460250797c8920b709/cli/command/container/stats_helpers.go#L166
func calculateDockerAbsoluteCpu(pStats container.CPUStats, stats container.CPUStats) float64 {
// Calculate the change in CPU usage between the current and previous reading.
cpuDelta := float64(stats.CPUUsage.TotalUsage) - float64(pStats.CPUUsage.TotalUsage)

// Calculate the change for the entire system's CPU usage between current and previous reading.
systemDelta := float64(stats.SystemUsage) - float64(pStats.SystemUsage)

// Calculate the total number of CPU cores being used.
cpus := float64(stats.OnlineCPUs)
if cpus == 0.0 {
cpus = float64(len(stats.CPUUsage.PercpuUsage))
// CPU time is compared to the elapsed time between samples because Podman's Docker-compatible API
// does not provide Docker-equivalent SystemUsage values.
func calculateDockerAbsoluteCpu(stats container.StatsResponse) float64 {
current := stats.CPUStats.CPUUsage.TotalUsage
previous := stats.PreCPUStats.CPUUsage.TotalUsage
if current <= previous || stats.PreRead.IsZero() || !stats.Read.After(stats.PreRead) {
return 0
}

percent := 0.0
if systemDelta > 0.0 && cpuDelta > 0.0 {
percent = (cpuDelta / systemDelta) * 100.0

if cpus > 0 {
percent *= cpus
}
}

return math.Round(percent*1000) / 1000
cpuDelta := float64(current - previous)
timeDelta := float64(stats.Read.Sub(stats.PreRead).Nanoseconds())
return math.Round((cpuDelta/timeDelta)*100*1000) / 1000
}
80 changes: 80 additions & 0 deletions environment/docker/stats_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package docker

import (
"testing"
"time"

"github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/require"
)

func TestCalculateDockerAbsoluteCpu(t *testing.T) {
base := time.Date(2026, time.July, 13, 12, 0, 0, 0, time.UTC)

tests := []struct {
name string
stats container.StatsResponse
expected float64
}{
{
name: "uses elapsed wall time",
stats: func() container.StatsResponse {
stats := cpuStatsResponse(base, 2*time.Second, 5_000_000_000, 6_000_000_000)
stats.PreCPUStats.SystemUsage = 10_000_000_000
stats.CPUStats.SystemUsage = 74_000_000_000
stats.CPUStats.OnlineCPUs = 64
return stats
}(),
expected: 50,
},
{
name: "multiple cores",
stats: cpuStatsResponse(base, time.Second, 5_000_000_000, 7_500_000_000),
expected: 250,
},
{
name: "rounds to three decimal places",
stats: cpuStatsResponse(base, 3*time.Second, 5_000_000_000, 6_000_000_000),
expected: 33.333,
},
{
name: "no CPU usage",
stats: cpuStatsResponse(base, time.Second, 5_000_000_000, 5_000_000_000),
expected: 0,
},
{
name: "CPU counter reset",
stats: cpuStatsResponse(base, time.Second, 5_000_000_000, 1_000_000_000),
expected: 0,
},
{
name: "missing previous timestamp",
stats: cpuStatsResponse(time.Time{}, time.Second, 5_000_000_000, 6_000_000_000),
expected: 0,
},
{
name: "non-increasing timestamp",
stats: cpuStatsResponse(base, 0, 5_000_000_000, 6_000_000_000),
expected: 0,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, calculateDockerAbsoluteCpu(tt.stats))
})
}
}

func cpuStatsResponse(preRead time.Time, elapsed time.Duration, previous, current uint64) container.StatsResponse {
return container.StatsResponse{
Read: preRead.Add(elapsed),
PreRead: preRead,
CPUStats: container.CPUStats{
CPUUsage: container.CPUUsage{TotalUsage: current},
},
PreCPUStats: container.CPUStats{
CPUUsage: container.CPUUsage{TotalUsage: previous},
},
}
}