|
| 1 | +//go:build linux || darwin |
1 | 2 | // +build linux darwin
|
2 | 3 |
|
3 | 4 | package checks
|
4 | 5 |
|
5 | 6 | import (
|
6 | 7 | "context"
|
7 |
| - "runtime" |
| 8 | + "fmt" |
| 9 | + "math" |
8 | 10 | "time"
|
9 | 11 |
|
10 | 12 | "github.com/shirou/gopsutil/v3/cpu"
|
11 | 13 | )
|
12 | 14 |
|
| 15 | +func saturatingSub(a, b float64) float64 { |
| 16 | + if a > b { |
| 17 | + return a - b |
| 18 | + } |
| 19 | + return 0 |
| 20 | +} |
| 21 | + |
| 22 | +func calcuateBusy(t cpu.TimesStat) (float64, float64, float64) { |
| 23 | + busy := t.User + t.Nice + t.System + t.Irq + t.Softirq + t.Steal |
| 24 | + idle := t.Idle + t.Iowait |
| 25 | + total := busy + idle |
| 26 | + return busy, idle, total |
| 27 | +} |
| 28 | + |
| 29 | +func calculateUsagePercentage(prev, current cpu.TimesStat) float64 { |
| 30 | + // This is highly inspired by |
| 31 | + // https://github.com/shirou/gopsutil/blob/master/cpu/cpu.go#L107 |
| 32 | + // and htop |
| 33 | + // https://github.com/htop-dev/htop/blob/main/linux/LinuxProcessList.c#L1948-L2006 |
| 34 | + // and yes - htop is reading /proc/stat as we do |
| 35 | + prevBusy, _, prevTotal := calcuateBusy(prev) |
| 36 | + currentBusy, _, currentTotal := calcuateBusy(current) |
| 37 | + |
| 38 | + if currentBusy <= prevBusy { |
| 39 | + return 0 |
| 40 | + } |
| 41 | + |
| 42 | + if currentTotal <= prevTotal { |
| 43 | + return 100 |
| 44 | + } |
| 45 | + return math.Min(100, math.Max(0, (currentBusy-prevBusy)/(currentTotal-prevTotal)*100)) |
| 46 | +} |
| 47 | + |
13 | 48 | // Run the actual check
|
14 | 49 | // if error != nil the check result will be nil
|
15 | 50 | // ctx can be canceled and runs the timeout
|
16 | 51 | // CheckResult will be serialized after the return and should not change until the next call to Run
|
17 | 52 | func (c *CheckCpu) Run(ctx context.Context) (interface{}, error) {
|
18 | 53 | result := &resultCpu{}
|
19 | 54 |
|
20 |
| - cpuPercentages, err := cpu.PercentWithContext(ctx, 1*time.Second, true) |
21 |
| - |
22 |
| - if err == nil { |
23 |
| - result.PercentagePerCore = cpuPercentages |
| 55 | + prevTimeStats, err := cpu.TimesWithContext(ctx, true) |
| 56 | + if err != nil { |
| 57 | + return nil, err |
24 | 58 | }
|
25 | 59 |
|
26 |
| - //get total CPU percentage |
27 |
| - cpuPercentageTotal, err := cpu.PercentWithContext(ctx, 1*time.Second, false) |
| 60 | + if err := c.SleepWithContext(ctx, 1*time.Second); err != nil { |
| 61 | + return nil, err |
| 62 | + } |
28 | 63 |
|
29 |
| - if err == nil { |
30 |
| - result.PercentageTotal = cpuPercentageTotal[0] |
| 64 | + timeStats, err := cpu.TimesWithContext(ctx, true) |
| 65 | + if err != nil { |
| 66 | + return nil, err |
31 | 67 | }
|
32 | 68 |
|
33 |
| - if runtime.GOOS == "linux" { |
34 |
| - timeStats, err := cpu.TimesWithContext(ctx, false) |
35 |
| - |
36 |
| - if err == nil { |
37 |
| - total := timeStats[0].User + timeStats[0].Nice + timeStats[0].System + timeStats[0].Idle + timeStats[0].Iowait + timeStats[0].Irq + timeStats[0].Softirq |
38 |
| - result.DetailsTotal = &cpuDetails{ |
39 |
| - User: timeStats[0].User / total * 100, |
40 |
| - Nice: timeStats[0].Nice / total * 100, |
41 |
| - System: timeStats[0].System / total * 100, |
42 |
| - Iowait: timeStats[0].Iowait / total * 100, |
43 |
| - } |
44 |
| - } |
| 69 | + if len(prevTimeStats) != len(timeStats) { |
| 70 | + return nil, fmt.Errorf("number of CPU cores has changed %v != %v", len(prevTimeStats), len(timeStats)) |
| 71 | + } |
45 | 72 |
|
46 |
| - //get timings per CPU |
47 |
| - var timings []cpuDetails |
48 |
| - timeStats, err = cpu.TimesWithContext(ctx, true) |
49 |
| - if err == nil { |
50 |
| - for _, timeStat := range timeStats { |
51 |
| - total := timeStat.User + timeStat.Nice + timeStat.System + timeStat.Idle + timeStat.Iowait + timeStat.Irq + timeStat.Softirq |
52 |
| - timings = append(timings, cpuDetails{ |
53 |
| - User: timeStat.User / total * 100, |
54 |
| - Nice: timeStat.Nice / total * 100, |
55 |
| - System: timeStat.System / total * 100, |
56 |
| - Idle: timeStat.Idle / total * 100, |
57 |
| - Iowait: timeStat.Iowait / total * 100, |
58 |
| - }) |
59 |
| - } |
60 |
| - result.DetailsPerCore = timings |
61 |
| - } |
| 73 | + // Get CPU usage per core |
| 74 | + var cpuPercentages []float64 |
| 75 | + for i := range prevTimeStats { |
| 76 | + cpuPercentage := calculateUsagePercentage(prevTimeStats[i], timeStats[i]) |
| 77 | + cpuPercentages = append(cpuPercentages, cpuPercentage) |
62 | 78 | }
|
| 79 | + result.PercentagePerCore = cpuPercentages |
63 | 80 |
|
64 |
| - if runtime.GOOS == "darwin" { |
65 |
| - timeStats, err := cpu.TimesWithContext(ctx, false) |
66 |
| - |
67 |
| - if err == nil { |
68 |
| - total := timeStats[0].User + timeStats[0].Nice + timeStats[0].System + timeStats[0].Idle |
69 |
| - result.DetailsTotal = &cpuDetails{ |
70 |
| - User: timeStats[0].User / total * 100, |
71 |
| - Nice: timeStats[0].Nice / total * 100, |
72 |
| - System: timeStats[0].System / total * 100, |
73 |
| - Idle: timeStats[0].Idle / total * 100, |
74 |
| - } |
| 81 | + // Get total CPU usage as percentage |
| 82 | + var totalCpuPercentage float64 |
| 83 | + for i := range cpuPercentages { |
| 84 | + totalCpuPercentage = totalCpuPercentage + cpuPercentages[i] |
| 85 | + } |
| 86 | + result.PercentageTotal = totalCpuPercentage / float64(len(cpuPercentages)) |
| 87 | + |
| 88 | + // Get CPU timing details per core |
| 89 | + var timings []cpuDetails |
| 90 | + var totalUser, totalNice, totalSystem, totalIoWait, totalIdle, totalTotal float64 |
| 91 | + for i := range prevTimeStats { |
| 92 | + // Ignore t.Irq + t.Softirq + t.Steal to get 100% over all values we list in the json |
| 93 | + prevTotal := prevTimeStats[i].User + prevTimeStats[i].Nice + prevTimeStats[i].System + prevTimeStats[i].Idle + prevTimeStats[i].Iowait |
| 94 | + currentTotal := timeStats[i].User + timeStats[i].Nice + timeStats[i].System + timeStats[i].Idle + timeStats[i].Iowait |
| 95 | + |
| 96 | + userDelta := saturatingSub(timeStats[i].User, prevTimeStats[i].User) |
| 97 | + niceDelta := saturatingSub(timeStats[i].Nice, prevTimeStats[i].Nice) |
| 98 | + systemDelta := saturatingSub(timeStats[i].System, prevTimeStats[i].System) |
| 99 | + ioWaitDelta := saturatingSub(timeStats[i].Iowait, prevTimeStats[i].Iowait) |
| 100 | + idleDelta := saturatingSub(timeStats[i].Idle, prevTimeStats[i].Idle) |
| 101 | + totalDelta := saturatingSub(currentTotal, prevTotal) |
| 102 | + |
| 103 | + if totalDelta == 0 { |
| 104 | + timings = append(timings, cpuDetails{}) |
| 105 | + } else { |
| 106 | + // Do not Division by zero (not sure if this could ever happen to be honest but better safe than sorry) |
| 107 | + timings = append(timings, cpuDetails{ |
| 108 | + User: userDelta / totalDelta * 100, |
| 109 | + Nice: niceDelta / totalDelta * 100, |
| 110 | + System: systemDelta / totalDelta * 100, |
| 111 | + Idle: idleDelta / totalDelta * 100, |
| 112 | + Iowait: ioWaitDelta / totalDelta * 100, |
| 113 | + }) |
75 | 114 | }
|
76 | 115 |
|
77 |
| - //get timings per CPU |
78 |
| - var timings []cpuDetails |
79 |
| - timeStats, err = cpu.TimesWithContext(ctx, true) |
80 |
| - if err == nil { |
81 |
| - for _, timeStat := range timeStats { |
82 |
| - total := timeStat.User + timeStat.Nice + timeStat.System + timeStat.Idle |
83 |
| - timings = append(timings, cpuDetails{ |
84 |
| - User: timeStat.User / total * 100, |
85 |
| - Nice: timeStat.Nice / total * 100, |
86 |
| - System: timeStat.System / total * 100, |
87 |
| - Idle: timeStat.Idle / total * 100, |
88 |
| - }) |
89 |
| - } |
90 |
| - result.DetailsPerCore = timings |
91 |
| - } |
| 116 | + totalUser = totalUser + userDelta |
| 117 | + totalNice = totalNice + niceDelta |
| 118 | + totalSystem = totalSystem + systemDelta |
| 119 | + totalIoWait = totalIoWait + ioWaitDelta |
| 120 | + totalIdle = totalIdle + idleDelta |
| 121 | + totalTotal = totalTotal + totalDelta |
| 122 | + } |
| 123 | + result.DetailsPerCore = timings |
| 124 | + |
| 125 | + // Get total CPU timing details |
| 126 | + result.DetailsTotal = &cpuDetails{ |
| 127 | + User: totalUser / totalTotal * 100, |
| 128 | + Nice: totalNice / totalTotal * 100, |
| 129 | + System: totalSystem / totalTotal * 100, |
| 130 | + Idle: totalIdle / totalTotal * 100, |
| 131 | + Iowait: totalIoWait / totalTotal * 100, |
92 | 132 | }
|
93 | 133 |
|
94 | 134 | return result, nil
|
95 | 135 |
|
96 | 136 | }
|
| 137 | + |
| 138 | +// Like sleep but can be canceled by context |
| 139 | +func (c *CheckCpu) SleepWithContext(ctx context.Context, interval time.Duration) error { |
| 140 | + var timer = time.NewTimer(interval) |
| 141 | + select { |
| 142 | + case <-ctx.Done(): |
| 143 | + return ctx.Err() |
| 144 | + case <-timer.C: |
| 145 | + return nil |
| 146 | + } |
| 147 | +} |
0 commit comments