-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cpu): Add cpu utilization component (#16)
The cpu utilization component will calculate current system cpu load by reading /proc/stat and interpreting the values. The load will be rendered as percentage of idle vs load.
- Loading branch information
1 parent
bb06727
commit 0b609ae
Showing
5 changed files
with
316 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
package utilization | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"os" | ||
"strconv" | ||
"strings" | ||
"sync/atomic" | ||
"time" | ||
|
||
"github.com/freddiehaddad/swaybar/pkg/descriptor" | ||
) | ||
|
||
// See: man proc for /proc/stat column value meanings | ||
const ( | ||
user = iota | ||
nice | ||
system | ||
idle | ||
iowait | ||
irq | ||
softirq | ||
steal | ||
guest | ||
guest_nice | ||
num_fields | ||
) | ||
|
||
type CPUUtilization struct { | ||
Interval time.Duration | ||
PrevStatsValues []int64 | ||
Enabled atomic.Bool | ||
} | ||
|
||
func New(interval time.Duration) (*CPUUtilization, error) { | ||
cpu := &CPUUtilization{ | ||
Interval: interval, | ||
PrevStatsValues: make([]int64, num_fields), | ||
} | ||
|
||
cpu.Update() | ||
return cpu, nil | ||
} | ||
|
||
func (c *CPUUtilization) Update() (descriptor.Descriptor, error) { | ||
log.Println("Updating CPU utilization") | ||
descriptor := descriptor.Descriptor{ | ||
Component: "cpuutil", | ||
Value: "", | ||
} | ||
var sb strings.Builder | ||
|
||
statPath := "/proc/stat" | ||
statRaw, err := os.ReadFile(statPath) | ||
if err != nil { | ||
log.Println("Error reading", statPath, err) | ||
return descriptor, err | ||
} | ||
|
||
currentStatValuesRaw, err := getStatValues(statRaw) | ||
if err != nil { | ||
log.Println(err) | ||
return descriptor, err | ||
} | ||
|
||
currentStatValues, err := parseInts(currentStatValuesRaw) | ||
if err != nil { | ||
log.Printf("Error parsing currentStatValuesRaw=%v, err=%s\n", currentStatValuesRaw, err) | ||
return descriptor, err | ||
} | ||
|
||
previousStatValuesSum := sumArray(c.PrevStatsValues) | ||
currentStatValuesSum := sumArray(currentStatValues) | ||
|
||
previousIdleValue := c.PrevStatsValues[idle] | ||
currentIdleValue := currentStatValues[idle] | ||
|
||
utilizationDelta := currentStatValuesSum - previousStatValuesSum | ||
idleDelta := currentIdleValue - previousIdleValue | ||
|
||
netUtilizationDelta := utilizationDelta - idleDelta | ||
|
||
cpuUtilization := 100.0 * float64(netUtilizationDelta) / float64(utilizationDelta) | ||
|
||
c.PrevStatsValues = currentStatValues | ||
|
||
sb.WriteString(fmt.Sprintf("CPU %5.1f%%", cpuUtilization)) | ||
descriptor.Value = sb.String() | ||
return descriptor, nil | ||
} | ||
|
||
func (c *CPUUtilization) Start(buffer chan descriptor.Descriptor) { | ||
c.Enabled.Store(true) | ||
|
||
go func() { | ||
for c.Enabled.Load() { | ||
descriptor, err := c.Update() | ||
if err != nil { | ||
log.Println("Error during update", err) | ||
} else { | ||
buffer <- descriptor | ||
} | ||
time.Sleep(c.Interval) | ||
} | ||
}() | ||
} | ||
|
||
func (c *CPUUtilization) Stop() { | ||
c.Enabled.Store(false) | ||
} | ||
|
||
func getStatValues(bytes []byte) ([]string, error) { | ||
const expectedLength = 2 | ||
const numSplits = 2 | ||
const rawSeparator = "\n" | ||
const valueSeparator = " " | ||
const valuesExpected = 10 | ||
const firstValue = "cpu" | ||
|
||
finalValues := []string{} | ||
|
||
s := string(bytes) | ||
split := strings.SplitAfterN(s, rawSeparator, numSplits) | ||
if len(split) != 2 { | ||
err := fmt.Errorf("error splitting %s, expected a length %d, but got length %d", s, expectedLength, len(split)) | ||
log.Println(err) | ||
return finalValues, err | ||
} | ||
|
||
stats := split[0] | ||
log.Println("Prepping", stats) | ||
|
||
if len(stats) <= len(firstValue) { | ||
err := fmt.Errorf("length of %s: %d is not as expected", stats, len(stats)) | ||
return finalValues, err | ||
} | ||
|
||
stats = strings.TrimPrefix(stats, firstValue) | ||
stats = strings.TrimSpace(stats) | ||
|
||
log.Println("Finished prepping", stats) | ||
|
||
values := strings.Split(stats, valueSeparator) | ||
if len(values) != valuesExpected { | ||
err := fmt.Errorf("error procesing values, expected length %d, but got length %d", valuesExpected, len(values)) | ||
return finalValues, err | ||
} | ||
|
||
return values, nil | ||
} | ||
|
||
func sumArray(values []int64) int64 { | ||
sum := int64(0) | ||
|
||
for _, value := range values { | ||
sum += value | ||
} | ||
|
||
return sum | ||
} | ||
|
||
func parseInts(values []string) ([]int64, error) { | ||
intValues := make([]int64, len(values)) | ||
for index, value := range values { | ||
intValue, err := strconv.ParseInt(value, 10, 32) | ||
if err != nil { | ||
return intValues, err | ||
} | ||
intValues[index] = intValue | ||
} | ||
return intValues, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package utilization | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
) | ||
|
||
func TestParseInts(t *testing.T) { | ||
tests := []struct { | ||
input []string | ||
expected []int64 | ||
errExpected bool | ||
}{ | ||
{[]string{}, []int64{}, false}, | ||
{[]string{"0"}, []int64{0}, false}, | ||
{[]string{"0", "1"}, []int64{0, 1}, false}, | ||
{[]string{"a", "1"}, []int64{}, true}, | ||
{[]string{"1", "a"}, []int64{}, true}, | ||
} | ||
|
||
for index, test := range tests { | ||
results, err := parseInts(test.input) | ||
|
||
if test.errExpected { | ||
if err == nil { | ||
t.Errorf("Test %d failed. Expected err, got err=nil", index) | ||
} | ||
continue | ||
} | ||
|
||
if err != nil { | ||
t.Errorf("Test %d failed. Expected passing test, got err=%s", index, err) | ||
continue | ||
} | ||
|
||
if len(results) != len(test.expected) { | ||
t.Errorf("Test %d failed. Expected length=%d, got length=%d\n", index, len(test.expected), len(results)) | ||
continue | ||
} | ||
|
||
for i, result := range results { | ||
if test.expected[i] != result { | ||
t.Errorf("Test %d failed at i=%d. Expected value=%d, got value=%d\n", index, i, test.expected[i], result) | ||
} | ||
} | ||
} | ||
} | ||
|
||
func TestSumArray(t *testing.T) { | ||
tests := []struct { | ||
input []int64 | ||
sum int64 | ||
}{ | ||
{[]int64{}, 0}, | ||
{[]int64{0}, 0}, | ||
{[]int64{0, 0}, 0}, | ||
{[]int64{1}, 1}, | ||
{[]int64{0, 1}, 1}, | ||
{[]int64{1, 0}, 1}, | ||
{[]int64{1, 1}, 2}, | ||
} | ||
|
||
for index, test := range tests { | ||
sum := sumArray(test.input) | ||
if sum != test.sum { | ||
t.Errorf("Test %d failed. Expected sum=%d, got sum=%d", index, test.sum, sum) | ||
} | ||
} | ||
} | ||
|
||
func TestGetStatValues(t *testing.T) { | ||
tests := []struct { | ||
input []byte | ||
expected []string | ||
err error | ||
}{ | ||
{ | ||
[]byte("cpu 0 0 0 0 0 0 0 0 0 0\n"), | ||
[]string{"0", "0", "0", "0", "0", "0", "0", "0", "0", "0"}, | ||
nil, | ||
}, | ||
{ | ||
[]byte("cpu 1528029 235 1406982 142714174 167167 223736 40373 0 0 0\n"), | ||
[]string{"1528029", "235", "1406982", "142714174", "167167", "223736", "40373", "0", "0", "0"}, | ||
nil, | ||
}, | ||
{ | ||
[]byte("cpu"), | ||
nil, | ||
fmt.Errorf("error splitting cpu, expected a length 2, but got length 1"), | ||
}, | ||
{ | ||
[]byte("cpu\n"), | ||
nil, | ||
fmt.Errorf("error procesing values, expected length 10, but got length 1"), | ||
}, | ||
{ | ||
[]byte("cpu 0 0 0 0 0 0 0 0 0\n"), | ||
nil, | ||
fmt.Errorf("error procesing values, expected length 10, but got length 9"), | ||
}, | ||
} | ||
|
||
for index, test := range tests { | ||
result, err := getStatValues(test.input) | ||
if test.err != nil && test.err.Error() != err.Error() { | ||
t.Errorf("Test %d failed. Expected err=%v, got err=%v", index, test.err, err) | ||
continue | ||
} | ||
|
||
if len(test.expected) != len(result) { | ||
t.Errorf("Test %d failed. Expected result len=%d, got len=%d", index, len(test.expected), len(result)) | ||
continue | ||
} | ||
|
||
for i, s := range test.expected { | ||
if s != result[i] { | ||
t.Errorf("Test %d failed. Expected result[%d]=%s, got=%s", index, i, s, result[i]) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters