diff --git a/daemon/stats_unix.go b/daemon/stats_unix.go index f734f66b9e..ed8ac35439 100644 --- a/daemon/stats_unix.go +++ b/daemon/stats_unix.go @@ -6,6 +6,7 @@ import ( "bufio" "context" "fmt" + "io" "os" "strconv" "strings" @@ -311,30 +312,29 @@ const ( nanoSecondsPerSecond = 1e9 ) -var procStatPath = "/proc/stat" - -// getSystemCPUUsage returns the host system's cpu usage in -// nanoseconds and number of online CPUs. An error is returned -// if the format of the underlying file does not match. -// -// Uses /proc/stat defined by POSIX. Looks for the cpu -// statistics line and then sums up the first seven fields -// provided. See `man 5 proc` for details on specific field -// information. +// getSystemCPUUsage reads the system's CPU usage from /proc/stat and returns +// the total CPU usage in nanoseconds and the number of CPUs. func getSystemCPUUsage() (cpuUsage uint64, cpuNum uint32, _ error) { - f, err := os.Open(procStatPath) + f, err := os.Open("/proc/stat") if err != nil { return 0, 0, err } defer f.Close() - rdr := bufio.NewReaderSize(f, 1024) + return readSystemCPUUsage(f) +} + +// readSystemCPUUsage parses CPU usage information from a reader providing +// /proc/stat format data. It returns the total CPU usage in nanoseconds +// and the number of CPUs. +func readSystemCPUUsage(r io.Reader) (cpuUsage uint64, cpuNum uint32, _ error) { + rdr := bufio.NewReaderSize(r, 1024) for { data, isPartial, err := rdr.ReadLine() if err != nil { - return 0, 0, fmt.Errorf("error scanning '%s' file: %w", procStatPath, err) + return 0, 0, fmt.Errorf("error scanning /proc/stat file: %w", err) } // Assume all cpu* records are at the start of the file, like glibc: // https://github.com/bminor/glibc/blob/5d00c201b9a2da768a79ea8d5311f257871c0b43/sysdeps/unix/sysv/linux/getsysstats.c#L108-L135 @@ -358,8 +358,7 @@ func getSystemCPUUsage() (cpuUsage uint64, cpuNum uint32, _ error) { } totalClockTicks += v } - cpuUsage = (totalClockTicks * nanoSecondsPerSecond) / - clockTicksPerSecond + cpuUsage = (totalClockTicks * nanoSecondsPerSecond) / clockTicksPerSecond } if '0' <= line[3] && line[3] <= '9' { cpuNum++ diff --git a/daemon/stats_unix_test.go b/daemon/stats_unix_test.go index 847e1a1e4a..277e68d4f6 100644 --- a/daemon/stats_unix_test.go +++ b/daemon/stats_unix_test.go @@ -3,28 +3,20 @@ package daemon import ( - "os" - "path/filepath" + _ "embed" + "strings" "testing" "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" ) +//go:embed testdata/stat +var statData string + func TestGetSystemCPUUsageParsing(t *testing.T) { - dummyFilePath := filepath.Join("testdata", "stat") - expectedCpuUsage := uint64(65647090000000) - expectedCpuNum := uint32(128) - - origStatPath := procStatPath - procStatPath = dummyFilePath - defer func() { procStatPath = origStatPath }() - - _, err := os.Stat(dummyFilePath) - assert.NilError(t, err) - - cpuUsage, cpuNum, err := getSystemCPUUsage() - - assert.Equal(t, cpuUsage, expectedCpuUsage) - assert.Equal(t, cpuNum, expectedCpuNum) - assert.NilError(t, err) + input := strings.NewReader(statData) + cpuUsage, cpuNum, _ := readSystemCPUUsage(input) + assert.Check(t, is.Equal(cpuUsage, uint64(65647090000000))) + assert.Check(t, is.Equal(cpuNum, uint32(128))) }