Skip to content

Commit

Permalink
feat(cpu): Add cpu utilization component (#16)
Browse files Browse the repository at this point in the history
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
freddiehaddad authored Oct 30, 2023
1 parent bb06727 commit 0b609ae
Show file tree
Hide file tree
Showing 5 changed files with 316 additions and 6 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ components report updates at different intervals.
List of components currently implemented included:

- CPU
- CPU Utilization
- GPU
- Time
- Network

## Sample Output

```text
D 977.58 Kbps U 36.79 Mbps | CPU 26.0 °C | GPU 58.0 °C | Wednesday, 25-Oct-23 16:14:20 PDT
D 977.58 Kbps U 36.79 Mbps | CPU 0.9% | CPU 26.0 °C | GPU 58.0 °C | Wednesday, 25-Oct-23 16:14:20 PDT
```

## Building
Expand Down
10 changes: 7 additions & 3 deletions config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,21 @@ network:
order: 0
device: "enp6s0"

cpuutil:
interval: "1s"
order: 1

cputemp:
# interval: "500ms" # equivalent to 0.5 seconds
order: 1
order: 2
sensor: "temp1_input"

gpu:
# interval: "2s" # 2 seconds
order: 2
order: 3
sensor: "temp1_input"

date:
# interval: "1500000000ns" # equivalent to 1.5 seconds
order: 3
order: 4
format: "Monday, 02-Jan-06 15:04:05 MST"
173 changes: 173 additions & 0 deletions pkg/cpu/utilization/utilization.go
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
}
122 changes: 122 additions & 0 deletions pkg/cpu/utilization/utilization_test.go
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])
}
}
}
}
14 changes: 12 additions & 2 deletions pkg/main/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import (
"strings"
"time"

"github.com/freddiehaddad/swaybar/pkg/cpu/temp"
cputemp "github.com/freddiehaddad/swaybar/pkg/cpu/temp"
cpuutil "github.com/freddiehaddad/swaybar/pkg/cpu/utilization"
"github.com/freddiehaddad/swaybar/pkg/date"
"github.com/freddiehaddad/swaybar/pkg/descriptor"
"github.com/freddiehaddad/swaybar/pkg/gpu"
Expand Down Expand Up @@ -41,7 +42,7 @@ func ParseInterval(interval string) (time.Duration, error) {
return parsed, err
}

func GenerateRenderOrder(components map[string]Component, order []string) (error) {
func GenerateRenderOrder(components map[string]Component, order []string) error {
for component, settings := range components {
order[settings.Order] = component
}
Expand Down Expand Up @@ -91,6 +92,15 @@ func main() {
}
cputemp, _ := cputemp.New(settings.Sensor, interval)
components["cputemp"] = cputemp
case "cpuutil":
log.Println("Creating cpu utilization component")
interval, err := ParseInterval(settings.Interval)
if err != nil {
log.Println("Failed to parse interval", interval, err, "using default value of 1s")
interval = time.Second
}
cpuutil, _ := cpuutil.New(interval)
components["cpuutil"] = cpuutil
case "gpu":
log.Println("Creating gpu component")
interval, err := ParseInterval(settings.Interval)
Expand Down

0 comments on commit 0b609ae

Please sign in to comment.