Skip to content

Commit

Permalink
Add memory.CPUTimesPercent()
Browse files Browse the repository at this point in the history
This new method returns a TimesStat that holds CPU usage
percentages for different work loads (e.g. user, system and so on).
  • Loading branch information
yudai committed Feb 25, 2020
1 parent 27358e8 commit c97a9d8
Show file tree
Hide file tree
Showing 2 changed files with 217 additions and 15 deletions.
113 changes: 99 additions & 14 deletions cpu/cpu.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import (
)

// TimesStat contains the amounts of time the CPU has spent performing different
// kinds of work. Time units are in USER_HZ or Jiffies (typically hundredths of
// a second). It is based on linux /proc/stat file.
// kinds of work. Time units are in USER_HZ, Jiffies (typically hundredths of
// a second) or percentages. It is based on linux /proc/stat file.
type TimesStat struct {
CPU string `json:"cpu"`
User float64 `json:"user"`
Expand Down Expand Up @@ -132,39 +132,124 @@ 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.
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) (t1, t2 []TimesStat, err error) {
if interval <= 0 {
return percentUsedFromLastCall(percpu)
return sampleAndSaveAsLast(ctx, percpu)
}

// Get CPU usage at the start of the interval.
cpuTimes1, err := Times(percpu)
t1, err = TimesWithContext(ctx, percpu)
if err != nil {
return nil, err
return
}

time.Sleep(interval)
select {
case <-time.After(interval):
case <-ctx.Done():
err = ctx.Err()
return
}

// And at the end of the interval.
cpuTimes2, err := Times(percpu)
t2, err = TimesWithContext(ctx, percpu)
if err != nil {
return nil, err
return
}

return calculateAllBusy(cpuTimes1, cpuTimes2)
return
}

func percentUsedFromLastCall(percpu bool) ([]float64, error) {
cpuTimes, err := Times(percpu)
func sampleAndSaveAsLast(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()
Expand All @@ -178,7 +263,7 @@ func percentUsedFromLastCall(percpu bool) ([]float64, error) {
}

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
}
119 changes: 118 additions & 1 deletion cpu/cpu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cpu

import (
"fmt"
"math"
"os"
"runtime"
"testing"
Expand Down Expand Up @@ -125,7 +126,6 @@ func testCPUPercent(t *testing.T, percpu bool) {
}

func testCPUPercentLastUsed(t *testing.T, percpu bool) {

numcpu := runtime.NumCPU()
testCount := 10

Expand Down Expand Up @@ -158,6 +158,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)
}
Expand All @@ -173,3 +274,19 @@ func TestCPUPercentIntervalZero(t *testing.T) {
func TestCPUPercentIntervalZeroPerCPU(t *testing.T) {
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)
}

0 comments on commit c97a9d8

Please sign in to comment.