Improve CPU usage parsing and error reporting

This fix address issues where the scanner was unable to properly parse longer outputs from /proc/stat. This could happen on an ARM machine with large amount of CPU cores (and interrupts). By switching to reader we have more control over data parsing and dump unnecessary data

Signed-off-by: Patrik Leifert <patrikleifert@hotmail.com>
This commit is contained in:
Patrik Leifert
2025-04-02 20:41:14 +02:00
parent c95e17638f
commit e22d04e8a9
3 changed files with 185 additions and 10 deletions

View File

@@ -311,6 +311,8 @@ 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.
@@ -320,17 +322,28 @@ const (
// provided. See `man 5 proc` for details on specific field
// information.
func getSystemCPUUsage() (cpuUsage uint64, cpuNum uint32, _ error) {
f, err := os.Open("/proc/stat")
f, err := os.Open(procStatPath)
if err != nil {
return 0, 0, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if len(line) < 4 || line[:3] != "cpu" {
break // Assume all cpu* records are at the front, like glibc https://github.com/bminor/glibc/blob/5d00c201b9a2da768a79ea8d5311f257871c0b43/sysdeps/unix/sysv/linux/getsysstats.c#L108-L135
rdr := bufio.NewReaderSize(f, 1024)
for {
data, isPartial, err := rdr.ReadLine()
if err != nil {
return 0, 0, fmt.Errorf("error scanning '%s' file: %w", procStatPath, 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
if isPartial || len(data) < 4 {
break
}
line := string(data)
if line[:3] != "cpu" {
break
}
if line[3] == ' ' {
parts := strings.Fields(line)
@@ -352,9 +365,5 @@ func getSystemCPUUsage() (cpuUsage uint64, cpuNum uint32, _ error) {
cpuNum++
}
}
if err := scanner.Err(); err != nil {
return 0, 0, fmt.Errorf("error scanning '/proc/stat' file: %w", err)
}
return cpuUsage, cpuNum, nil
}

30
daemon/stats_unix_test.go Normal file
View File

@@ -0,0 +1,30 @@
//go:build !windows
package daemon
import (
"os"
"path/filepath"
"testing"
"gotest.tools/v3/assert"
)
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)
}

136
daemon/testdata/stat vendored Normal file

File diff suppressed because one or more lines are too long