diff --git a/cpu/cpu.go b/cpu/cpu.go index 83bc23d..2d4fa61 100644 --- a/cpu/cpu.go +++ b/cpu/cpu.go @@ -142,6 +142,59 @@ func calculateAllBusy(t1, t2 []TimesStat) ([]float64, error) { return ret, nil } +func calculateItem(v1, v2, duration float64) float64 { + if v2 <= v1 { + return 0 + } + if duration <= 0 { + return 0 + } + + return math.Min(100, math.Max(0, (v2-v1)/(duration)*100)) +} + +func calculateItems(t1, t2 TimesStat) TimesStat { + duration := t2.Total() - t1.Total() + + items := TimesStat{ + CPU: t1.CPU, + User: calculateItem(t1.User, t2.User, duration), + System: calculateItem(t1.System, t2.System, duration), + Idle: calculateItem(t1.Idle, t2.Idle, duration), + Nice: calculateItem(t1.Nice, t2.Nice, duration), + Iowait: calculateItem(t1.Iowait, t2.Iowait, duration), + Irq: calculateItem(t1.Irq, t2.Irq, duration), + Softirq: calculateItem(t1.Softirq, t2.Softirq, duration), + Steal: calculateItem(t1.Steal, t2.Steal, duration), + Guest: calculateItem(t1.Guest, t2.Guest, duration), + GuestNice: calculateItem(t1.GuestNice, t2.GuestNice, duration), + } + + return items +} + +func calculateAllItems(t1, t2 []TimesStat) ([]TimesStat, error) { + // Make sure the CPU measurements have the same length. + if len(t1) != len(t2) { + return nil, fmt.Errorf( + "received two CPU counts: %d != %d", + len(t1), len(t2), + ) + } + + ret := make([]TimesStat, len(t1)) + for i, t := range t2 { + if t1[i].CPU != t.CPU { + return nil, fmt.Errorf( + "CPU number mismatch at %d: %s != %s", + i, t1[i].CPU, t.CPU, + ) + } + ret[i] = calculateItems(t1[i], t) + } + return ret, nil +} + // Percent calculates the percentage of cpu used either per CPU or combined. // If an interval of 0 is given it will compare the current cpu times against the last call. // Returns one value per cpu, or a single value if percpu is set to false. @@ -149,38 +202,65 @@ func Percent(interval time.Duration, percpu bool) ([]float64, error) { return PercentWithContext(context.Background(), interval, percpu) } +// CPUTimesPercent calculates the percentage of CPU time used for different type of work +// either per CPU or combined. +// If an interval of 0 is given it will compare the current cpu times against the last call. +// Returns one value per cpu, or a single value if percpu is set to false. +// When interval is too small, returned values can be all zero. +func CPUTimesPercent(interval time.Duration, percpu bool) ([]TimesStat, error) { + return CPUTimesPercentWithContext(context.Background(), interval, percpu) +} + func PercentWithContext(ctx context.Context, interval time.Duration, percpu bool) ([]float64, error) { + t1, t2, err := sample(ctx, interval, percpu) + if err != nil { + return nil, err + } + + return calculateAllBusy(t1, t2) +} + +func CPUTimesPercentWithContext(ctx context.Context, interval time.Duration, percpu bool) ([]TimesStat, error) { + t1, t2, err := sample(ctx, interval, percpu) + if err != nil { + return nil, err + } + + return calculateAllItems(t1, t2) +} + +func sample(ctx context.Context, interval time.Duration, percpu bool) (cpuTimes1, cpuTimes2 []TimesStat, err error) { if interval <= 0 { - return percentUsedFromLastCallWithContext(ctx, percpu) + return sampleAndSaveAsLastWithContext(ctx, percpu) } // Get CPU usage at the start of the interval. - cpuTimes1, err := TimesWithContext(ctx, percpu) + cpuTimes1, err = TimesWithContext(ctx, percpu) if err != nil { - return nil, err + return } if err := common.Sleep(ctx, interval); err != nil { - return nil, err + return nil, nil, err } // And at the end of the interval. - cpuTimes2, err := TimesWithContext(ctx, percpu) + cpuTimes2, err = TimesWithContext(ctx, percpu) if err != nil { - return nil, err + return } - return calculateAllBusy(cpuTimes1, cpuTimes2) + return } -func percentUsedFromLastCall(percpu bool) ([]float64, error) { - return percentUsedFromLastCallWithContext(context.Background(), percpu) +func sampleAndSaveAsLast(percpu bool) ([]TimesStat, []TimesStat, error) { + return sampleAndSaveAsLastWithContext(context.Background(), percpu) } -func percentUsedFromLastCallWithContext(ctx context.Context, percpu bool) ([]float64, error) { +func sampleAndSaveAsLastWithContext(ctx context.Context, percpu bool) ([]TimesStat, []TimesStat, error) { cpuTimes, err := TimesWithContext(ctx, percpu) if err != nil { - return nil, err + return nil, nil, err } lastCPUPercent.Lock() defer lastCPUPercent.Unlock() @@ -194,7 +274,7 @@ func percentUsedFromLastCallWithContext(ctx context.Context, percpu bool) ([]flo } if lastTimes == nil { - return nil, fmt.Errorf("error getting times for cpu percent. lastTimes was nil") + return nil, nil, fmt.Errorf("error getting times for cpu percent. lastTimes was nil") } - return calculateAllBusy(lastTimes, cpuTimes) + return lastTimes, cpuTimes, nil } diff --git a/cpu/cpu_test.go b/cpu/cpu_test.go index 688660a..2f5167c 100644 --- a/cpu/cpu_test.go +++ b/cpu/cpu_test.go @@ -3,6 +3,7 @@ package cpu import ( "errors" "fmt" + "math" "os" "runtime" "testing" @@ -195,6 +196,107 @@ func testCPUPercentLastUsed(t *testing.T, percpu bool) { } } +func checkTimesStatPercentages(t *testing.T, ps TimesStat, numcpu int) { + t.Helper() + + // Check for slightly greater then 100% to account for any rounding issues. + if ps.User < 0.0 || ps.User > 100.0001*float64(numcpu) { + t.Fatalf("CPUPercent value User is invalid: %f", ps.User) + } + if ps.System < 0.0 || ps.System > 100.0001*float64(numcpu) { + t.Fatalf("CPUPercent value System is invalid: %f", ps.System) + } + if ps.Idle < 0.0 || ps.Idle > 100.0001*float64(numcpu) { + t.Fatalf("CPUPercent value Idle is invalid: %f", ps.Idle) + } + if ps.Nice < 0.0 || ps.Nice > 100.0001*float64(numcpu) { + t.Fatalf("CPUPercent value Nice is invalid: %f", ps.Nice) + } + if ps.Iowait < 0.0 || ps.Iowait > 100.0001*float64(numcpu) { + t.Fatalf("CPUPercent value Iowait is invalid: %f", ps.Iowait) + } + if ps.Irq < 0.0 || ps.Irq > 100.0001*float64(numcpu) { + t.Fatalf("CPUPercent value Irq is invalid: %f", ps.Irq) + } + if ps.Softirq < 0.0 || ps.Softirq > 100.0001*float64(numcpu) { + t.Fatalf("CPUPercent value Softirq is invalid: %f", ps.Softirq) + } + if ps.Steal < 0.0 || ps.Steal > 100.0001*float64(numcpu) { + t.Fatalf("CPUPercent value Steal is invalid: %f", ps.Steal) + } + + total := ps.User + ps.System + ps.Idle + ps.Nice + ps.Iowait + + ps.Irq + ps.Softirq + ps.Steal + if math.Round(total) != 100 { + t.Fatalf("CPUPercent total is invalid: %f", total) + } + + if ps.Guest < 0.0 || ps.Guest > 100.0001*float64(numcpu) { + t.Fatalf("CPUPercent value Guest is invalid: %f", ps.Guest) + } + if ps.GuestNice < 0.0 || ps.GuestNice > 100.0001*float64(numcpu) { + t.Fatalf("CPUPercent value GuestNice is invalid: %f", ps.GuestNice) + } +} + +func testCPUTimesPercent(t *testing.T, percpu bool) { + numcpu := runtime.NumCPU() + testCount := 10 + + if runtime.GOOS != "windows" { + testCount = 2 + v, err := CPUTimesPercent(time.Millisecond, percpu) + if err != nil { + t.Errorf("error %v", err) + } + // Skip CircleCI which CPU num is different + if os.Getenv("CIRCLECI") != "true" { + if (percpu && len(v) != numcpu) || (!percpu && len(v) != 1) { + t.Fatalf("wrong number of entries from CPUPercent: %v", v) + } + } + } + for i := 0; i < testCount; i++ { + duration := time.Duration(100) * time.Millisecond + v, err := CPUTimesPercent(duration, percpu) + if err != nil { + t.Errorf("error %v", err) + } + for _, ps := range v { + checkTimesStatPercentages(t, ps, numcpu) + } + } +} + +func testCPUTimesPercentLastUsed(t *testing.T, percpu bool) { + numcpu := runtime.NumCPU() + testCount := 10 + + if runtime.GOOS != "windows" { + testCount = 2 + v, err := CPUTimesPercent(time.Millisecond, percpu) + if err != nil { + t.Errorf("error %v", err) + } + // Skip CircleCI which CPU num is different + if os.Getenv("CIRCLECI") != "true" { + if (percpu && len(v) != numcpu) || (!percpu && len(v) != 1) { + t.Fatalf("wrong number of entries from CPUPercent: %v", v) + } + } + } + for i := 0; i < testCount; i++ { + v, err := CPUTimesPercent(0, percpu) + if err != nil { + t.Errorf("error %v", err) + } + time.Sleep(100 * time.Millisecond) + for _, ps := range v { + checkTimesStatPercentages(t, ps, numcpu) + } + } +} + func TestCPUPercent(t *testing.T) { testCPUPercent(t, false) } @@ -204,9 +306,27 @@ func TestCPUPercentPerCpu(t *testing.T) { } func TestCPUPercentIntervalZero(t *testing.T) { + time.Sleep(time.Millisecond * 200) testCPUPercentLastUsed(t, false) } func TestCPUPercentIntervalZeroPerCPU(t *testing.T) { + time.Sleep(time.Millisecond * 200) testCPUPercentLastUsed(t, true) } + +func TestCPUTimesPercent(t *testing.T) { + testCPUTimesPercent(t, false) +} + +func TestCPUTimesPercentPerCpu(t *testing.T) { + testCPUTimesPercent(t, true) +} + +func TestCPUTimesPercentIntervalZero(t *testing.T) { + testCPUTimesPercentLastUsed(t, false) +} + +func TestCPUTimesPercentIntervalZeroPerCPU(t *testing.T) { + testCPUTimesPercentLastUsed(t, true) +}