Skip to content

Commit

Permalink
feat(linux): ✨ switch total cpu context switches and processes create…
Browse files Browse the repository at this point in the history
…d sensors from totals to rates

- total cpu context switches changes to cpu context switch rate
- total processes created changes to process creation rate
- split cpu usage worker and sensor code into separate files
  • Loading branch information
joshuar committed Sep 27, 2024
1 parent 2f5ed89 commit ed015e7
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 93 deletions.
124 changes: 31 additions & 93 deletions internal/linux/cpu/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,9 @@
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

//revive:disable:unused-receiver
package cpu

import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
Expand All @@ -25,56 +17,47 @@ import (
"github.com/joshuar/go-hass-agent/internal/linux"
)

const (
usageUpdateInterval = 10 * time.Second
usageUpdateJitter = 500 * time.Millisecond

usageWorkerID = "cpu_usage_sensors"

totalCPUString = "cpu"
)

var ErrParseCPUUsage = errors.New("could not parse CPU usage")

//nolint:lll
var times = [...]string{"user_time", "nice_time", "system_time", "idle_time", "iowait_time", "irq_time", "softirq_time", "steal_time", "guest_time", "guest_nice_time"}

type usageWorker struct {
boottime time.Time
path string
linux.PollingSensorWorker
clktck int64
type rateSensor struct {
*sensor.Entity
prevState uint64
}

func (w *usageWorker) UpdateDelta(_ time.Duration) {}

func (w *usageWorker) Sensors(_ context.Context) ([]sensor.Entity, error) {
return w.getStats()
}
func (s *rateSensor) update(delta time.Duration, valueStr string) {
valueInt, _ := strconv.ParseUint(valueStr, 10, 64) //nolint:errcheck // if we can't parse it, value will be 0.

func NewUsageWorker(ctx context.Context) (*linux.PollingSensorWorker, error) {
worker := linux.NewPollingWorker(usageWorkerID, usageUpdateInterval, usageUpdateJitter)

clktck, found := linux.CtxGetClkTck(ctx)
if !found {
return worker, fmt.Errorf("%w: no clktck value", linux.ErrInvalidCtx)
if uint64(delta.Seconds()) > 0 {
s.State = (valueInt - s.prevState) / uint64(delta.Seconds()) / 2
} else {
s.State = 0
}

boottime, found := linux.CtxGetBoottime(ctx)
if !found {
return worker, fmt.Errorf("%w: no boottime value", linux.ErrInvalidCtx)
}
s.Attributes["Total"] = valueInt

worker.PollingType = &usageWorker{
path: filepath.Join(linux.ProcFSRoot, "stat"),
boottime: boottime,
clktck: clktck,
}
s.prevState = valueInt
}

return worker, nil
func newRateSensor(name, icon, units string) *rateSensor {
return &rateSensor{
Entity: &sensor.Entity{
Name: name,
StateClass: types.StateClassMeasurement,
Category: types.CategoryDiagnostic,
Units: units,
EntityState: &sensor.EntityState{
ID: strcase.ToSnake(name),
Icon: icon,
Attributes: map[string]any{
"data_source": linux.DataSrcProcfs,
},
},
},
}
}

func (w *usageWorker) newUsageSensor(details []string, category types.Category) sensor.Entity {
func newUsageSensor(clktck int64, details []string, category types.Category) sensor.Entity {
var name, id string

switch {
Expand All @@ -87,7 +70,7 @@ func (w *usageWorker) newUsageSensor(details []string, category types.Category)
id = "core_" + num + "_cpu_usage"
}

value, attributes := generateUsageValues(w.clktck, details[1:])
value, attributes := generateUsageValues(clktck, details[1:])

return sensor.Entity{
Name: name,
Expand Down Expand Up @@ -128,7 +111,7 @@ func generateUsageValues(clktck int64, details []string) (float64, map[string]an
return value, attrs
}

func (w *usageWorker) newCountSensor(name, icon, valueStr string) sensor.Entity {
func newCountSensor(name, icon, valueStr string) sensor.Entity {
valueInt, _ := strconv.Atoi(valueStr) //nolint:errcheck // if we can't parse it, value will be 0.

return sensor.Entity{
Expand All @@ -145,48 +128,3 @@ func (w *usageWorker) newCountSensor(name, icon, valueStr string) sensor.Entity
},
}
}

func (w *usageWorker) getStats() ([]sensor.Entity, error) {
var sensors []sensor.Entity

statsFH, err := os.Open(w.path)
if err != nil {
return nil, fmt.Errorf("fetch cpu usage: %w", err)
}

defer statsFH.Close()

statsFile := bufio.NewScanner(statsFH)
for statsFile.Scan() {
// Set up word scanner for line.
line := bufio.NewScanner(bytes.NewReader(statsFile.Bytes()))
line.Split(bufio.ScanWords)
// Split line by words
var cols []string
for line.Scan() {
cols = append(cols, line.Text())
}

if len(cols) == 0 {
return sensors, ErrParseCPUUsage
}
// Create a sensor depending on the line.
switch {
case cols[0] == totalCPUString:
sensors = append(sensors, w.newUsageSensor(cols, types.CategoryDefault))
case strings.Contains(cols[0], "cpu"):
sensors = append(sensors, w.newUsageSensor(cols, types.CategoryDiagnostic))
sensors = append(sensors, newCPUFreqSensor(cols[0]))
case cols[0] == "ctxt":
sensors = append(sensors, w.newCountSensor("Total CPU Context Switches", "mdi:counter", cols[1]))
case cols[0] == "processes":
sensors = append(sensors, w.newCountSensor("Total Processes Created", "mdi:application-cog", cols[1]))
case cols[0] == "procs_running":
sensors = append(sensors, w.newCountSensor("Processes Running", "mdi:application-cog", cols[1]))
case cols[0] == "procs_blocked":
sensors = append(sensors, w.newCountSensor("Processes Blocked", "mdi:application-cog", cols[1]))
}
}

return sensors, nil
}
123 changes: 123 additions & 0 deletions internal/linux/cpu/usageWorker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright (c) 2024 Joshua Rich <[email protected]>
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

package cpu

import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"

"github.com/joshuar/go-hass-agent/internal/hass/sensor"
"github.com/joshuar/go-hass-agent/internal/hass/sensor/types"
"github.com/joshuar/go-hass-agent/internal/linux"
)

const (
usageUpdateInterval = 10 * time.Second
usageUpdateJitter = 500 * time.Millisecond

usageWorkerID = "cpu_usage"

totalCPUString = "cpu"
)

var ErrParseCPUUsage = errors.New("could not parse CPU usage")

type usageWorker struct {
boottime time.Time
path string
rateSensors map[string]*rateSensor
linux.PollingSensorWorker
clktck int64
delta time.Duration
}

func (w *usageWorker) UpdateDelta(delta time.Duration) {
w.delta = delta
}

func (w *usageWorker) Sensors(_ context.Context) ([]sensor.Entity, error) {
return w.getUsageStats()
}

func NewUsageWorker(ctx context.Context) (*linux.PollingSensorWorker, error) {
worker := linux.NewPollingWorker(usageWorkerID, usageUpdateInterval, usageUpdateJitter)

clktck, found := linux.CtxGetClkTck(ctx)
if !found {
return worker, fmt.Errorf("%w: no clktck value", linux.ErrInvalidCtx)
}

boottime, found := linux.CtxGetBoottime(ctx)
if !found {
return worker, fmt.Errorf("%w: no boottime value", linux.ErrInvalidCtx)
}

worker.PollingType = &usageWorker{
path: filepath.Join(linux.ProcFSRoot, "stat"),
boottime: boottime,
clktck: clktck,
rateSensors: map[string]*rateSensor{
"ctxt": newRateSensor("CPU Context Switch Rate", "mdi:counter", "ctx/s"),
"processes": newRateSensor("Processes Creation Rate", "mdi:application-cog", "processes/s"),
},
}

return worker, nil
}

func (w *usageWorker) getUsageStats() ([]sensor.Entity, error) {
var sensors []sensor.Entity

statsFH, err := os.Open(w.path)
if err != nil {
return nil, fmt.Errorf("fetch cpu usage: %w", err)
}

defer statsFH.Close()

statsFile := bufio.NewScanner(statsFH)
for statsFile.Scan() {
// Set up word scanner for line.
line := bufio.NewScanner(bytes.NewReader(statsFile.Bytes()))
line.Split(bufio.ScanWords)
// Split line by words
var cols []string
for line.Scan() {
cols = append(cols, line.Text())
}

if len(cols) == 0 {
return sensors, ErrParseCPUUsage
}
// Create a sensor depending on the line.
switch {
case cols[0] == totalCPUString:
sensors = append(sensors, newUsageSensor(w.clktck, cols, types.CategoryDefault))
case strings.Contains(cols[0], "cpu"):
sensors = append(sensors, newUsageSensor(w.clktck, cols, types.CategoryDiagnostic))
sensors = append(sensors, newCPUFreqSensor(cols[0]))
case cols[0] == "ctxt":
w.rateSensors["ctxt"].update(w.delta, cols[1])
sensors = append(sensors, *w.rateSensors["ctxt"].Entity)
case cols[0] == "processes":
w.rateSensors["processes"].update(w.delta, cols[1])
sensors = append(sensors, *w.rateSensors["processes"].Entity)
case cols[0] == "procs_running":
sensors = append(sensors, newCountSensor("Processes Running", "mdi:application-cog", cols[1]))
case cols[0] == "procs_blocked":
sensors = append(sensors, newCountSensor("Processes Blocked", "mdi:application-cog", cols[1]))
}
}

return sensors, nil
}

0 comments on commit ed015e7

Please sign in to comment.