From b1d4f3565dd5b2c871446b0f6c84aa2b540bd2f9 Mon Sep 17 00:00:00 2001 From: Dottere Date: Tue, 6 Oct 2026 00:10:52 +0200 Subject: [PATCH] added load average calculations and moved the testing from main to their own cli commands --- cmd/rpi5-health/main.go | 22 ----- internal/cli/cli.go | 26 +++++ .../errors/{UnsupportedOS.go => errors.go} | 0 internal/load_average/linux_loadavg.go | 55 +++++++++++ internal/load_average/loadavg.go | 48 +++++++++ internal/load_average/win_loadavg.go | 99 +++++++++++++++++++ internal/windows_system/win.go | 47 ++++++++- 7 files changed, 271 insertions(+), 26 deletions(-) rename internal/errors/{UnsupportedOS.go => errors.go} (100%) create mode 100644 internal/load_average/linux_loadavg.go create mode 100644 internal/load_average/loadavg.go create mode 100644 internal/load_average/win_loadavg.go diff --git a/cmd/rpi5-health/main.go b/cmd/rpi5-health/main.go index b9f891b..b726265 100644 --- a/cmd/rpi5-health/main.go +++ b/cmd/rpi5-health/main.go @@ -1,35 +1,13 @@ package main import ( - "log" "rpi5-health/internal/cli" "rpi5-health/internal/http_internal" - "rpi5-health/internal/memory" - "rpi5-health/internal/processor" - "rpi5-health/internal/uptime" ) func main() { srv := http_internal.NewAppServer() go srv.Start() - memInfo, _ := memory.GetRamInformation() - - const GB = 1024 * 1024 * 1024 - - log.Printf("Total memory: %.2fGB\n", float64(memInfo.SystemMemory)/GB) - log.Printf("Available memory: %.2fGB\n", float64(memInfo.AvailableMemory)/GB) - log.Printf("Used memory: %.2fGB\n", float64(memInfo.GetUsedMemory())/GB) - - _, kernel, usage, idle := processor.GetCPUInformation().Usage() - - log.Printf("Total time spent idling: %.2f%%\n", idle) - log.Printf("Total time spent in kernel mode: %.2f%%\n", kernel) - log.Printf("Total time spent in user mode: %.2f%%\n", usage) - - uptime, _ := uptime.GetUptimeInformation() - - log.Printf("Total uptime: %s", uptime.FormatSeconds()) - cli.Listen() } diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 16725ac..e50ad89 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -5,6 +5,10 @@ import ( "fmt" "log" "os" + loadaverage "rpi5-health/internal/load_average" + "rpi5-health/internal/memory" + "rpi5-health/internal/processor" + "rpi5-health/internal/uptime" "strings" ) @@ -30,6 +34,28 @@ func Listen() { switch text { case "exit": return + case "meminfo": + memInfo, _ := memory.GetRamInformation() + + const GB = 1024 * 1024 * 1024 + + fmt.Printf("Total memory: %.2fGB\n", float64(memInfo.SystemMemory)/GB) + fmt.Printf("Available memory: %.2fGB\n", float64(memInfo.AvailableMemory)/GB) + fmt.Printf("Used memory: %.2fGB\n", float64(memInfo.GetUsedMemory())/GB) + case "cpuinfo": + _, kernel, usage, idle := processor.GetCPUInformation().Usage() + + fmt.Printf("Total time spent idling: %.2f%%\n", idle) + fmt.Printf("Total time spent in kernel mode: %.2f%%\n", kernel) + fmt.Printf("Total time spent in user mode: %.2f%%\n", usage) + case "uptime": + uptime, _ := uptime.GetUptimeInformation() + + fmt.Printf("Total uptime: %s\n", uptime.FormatSeconds()) + case "loadavg": + loadAvg, _ := loadaverage.GetLoadAverage() + + fmt.Printf("Load average: L1: %.2f | L5: %.2f | L15: %.2f\n", loadAvg.Load1, loadAvg.Load5, loadAvg.Load15) } } diff --git a/internal/errors/UnsupportedOS.go b/internal/errors/errors.go similarity index 100% rename from internal/errors/UnsupportedOS.go rename to internal/errors/errors.go diff --git a/internal/load_average/linux_loadavg.go b/internal/load_average/linux_loadavg.go new file mode 100644 index 0000000..07d4dda --- /dev/null +++ b/internal/load_average/linux_loadavg.go @@ -0,0 +1,55 @@ +//go:build linux + +package loadaverage + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +func getLinuxLoadAverage() (LoadAverageInformation, error) { + loadFile, err := os.Open("/proc/loadavg") + if err != nil { + return LoadAverageInformation{}, fmt.Errorf("Could not find /proc/loadavg. Are you sure you are on linux?") + } + defer loadFile.Close() + + fileReader := bufio.NewScanner(loadFile) + + var one_m float64 + var five_m float64 + var fifteen_m float64 + + fileReader.Scan() + if fileReader.Err() != nil { + return LoadAverageInformation{}, fmt.Errorf("Could not read /proc/uptime.") + } + + data := fileReader.Text() + + /* + * fields[0] One minute load average + * fields[1] Five minute load average + * fields[2] Fifteen minute load average + * fields[3] Currently executing/total scheduled + * fields[4] Most recent process PID + */ + fields := strings.Split(data, " ") + + one_m, _ = strconv.ParseFloat(fields[0], 64) + five_m, _ = strconv.ParseFloat(fields[1], 64) + fifteen_m, _ = strconv.ParseFloat(fields[2], 64) + + return LoadAverageInformation{ + Load1: one_m, + Load5: five_m, + Load15: fifteen_m, + }, nil +} + +func GetLoadAverage() (LoadAverageInformation, error) { + return getLinuxLoadAverage() +} diff --git a/internal/load_average/loadavg.go b/internal/load_average/loadavg.go new file mode 100644 index 0000000..00a5c8c --- /dev/null +++ b/internal/load_average/loadavg.go @@ -0,0 +1,48 @@ +package loadaverage + +import ( + "math" + "time" +) + +// Constants for calculating EMA + +const SAMPLE_DELAY time.Duration = time.Second * 5 + +var ( + ONE_MIN_ALPHA float64 = math.Exp(-SAMPLE_DELAY.Seconds() / 60.0) // e^(-dt/W), where dt is the sample time and W + FIVE_MIN_ALPHA float64 = math.Exp(-SAMPLE_DELAY.Seconds() / 300.0) // is the target timeframe. eg.: 60 for one min + FIFTEEN_MIN_ALPHA float64 = math.Exp(-SAMPLE_DELAY.Seconds() / 900.0) +) + +type LoadAverageInformation struct { + /* + * Load average in the past minute + */ + Load1 float64 + /* + * Load average in the past 5 minutes + */ + Load5 float64 + /* + * Load average in the past 15 minutes + */ + Load15 float64 + + initialized bool // track whether this is the first measurement or not +} + +func (loadAvg *LoadAverageInformation) Update(currentSample float64) { + if !loadAvg.initialized { + // Seed the initial averages with the first sample to avoid a slow ramp-up from zero + loadAvg.Load1 = currentSample + loadAvg.Load5 = currentSample + loadAvg.Load15 = currentSample + loadAvg.initialized = true + return + } + + loadAvg.Load1 = loadAvg.Load1*ONE_MIN_ALPHA + currentSample*(1.0-ONE_MIN_ALPHA) + loadAvg.Load5 = loadAvg.Load5*FIVE_MIN_ALPHA + currentSample*(1.0-FIVE_MIN_ALPHA) + loadAvg.Load15 = loadAvg.Load15*FIFTEEN_MIN_ALPHA + currentSample*(1.0-FIFTEEN_MIN_ALPHA) +} diff --git a/internal/load_average/win_loadavg.go b/internal/load_average/win_loadavg.go new file mode 100644 index 0000000..db8a075 --- /dev/null +++ b/internal/load_average/win_loadavg.go @@ -0,0 +1,99 @@ +//go:build windows + +package loadaverage + +import ( + "fmt" + "rpi5-health/internal/windows_system" + "sync" + "syscall" + "time" + "unsafe" +) + +const ( + // Formatting flag for PdhGetFormattedCounterValue to return a float64 + PDH_FMT_DOUBLE = 0x00000200 + ERROR_SUCCESS = 0 +) + +// PDH_FMT_COUNTERVALUE_DOUBLE maps the C struct _PDH_FMT_COUNTERVALUE for double values. +// * Note the explicit padding: C++ unions align to 8 bytes on 64-bit systems. +type PDH_FMT_COUNTERVALUE_DOUBLE struct { + CStatus uint32 + Pad uint32 // ! Crucial padding for 64-bit alignment before the float64 + DoubleValue float64 // Represents the union where the double is stored +} + +var ( + GlobalLoadAverage LoadAverageInformation + + // 1. A RWMutex protects the struct from concurrent reads/writes + loadMu sync.RWMutex + + startWorker = sync.OnceFunc(func() { + go loadAverageWorker() + }) +) + +func loadAverageWorker() { + var hQuery syscall.Handle // PDH_HQUERY + var hCounter syscall.Handle // PDH_HCOUNTER + + wintools := windows_system.GetWindowsTools() + + // initialize query + ret, _, _ := wintools.ProcPdhOpenQuery.Call(0, 0, uintptr(unsafe.Pointer(&hQuery))) + if ret != ERROR_SUCCESS { + fmt.Printf("PdhOpenQueryW failed: %x\n", ret) + return + } + defer wintools.ProcPdhCloseQuery.Call(uintptr(hQuery)) + + counterPath, _ := syscall.UTF16PtrFromString(`\Processor(_Total)\% Processor Time`) + ret, _, _ = wintools.ProcPdhAddCounter.Call( + uintptr(hQuery), + uintptr(unsafe.Pointer(counterPath)), + 0, + uintptr(unsafe.Pointer(&hCounter)), + ) + if ret != ERROR_SUCCESS { + fmt.Printf("PdhAddCounterW failed: %x\n", ret) + return + } + + // Collect initial data, "priming the counter" + wintools.ProcPdhCollectQueryData.Call(uintptr(hQuery)) + + // Infinite collection loop + for { + time.Sleep(SAMPLE_DELAY) + + wintools.ProcPdhCollectQueryData.Call(uintptr(hQuery)) + + var counterType uint32 + var value PDH_FMT_COUNTERVALUE_DOUBLE + + ret, _, _ = wintools.ProcPdhGetFormattedCounterValue.Call( + uintptr(hCounter), + uintptr(PDH_FMT_DOUBLE), + uintptr(unsafe.Pointer(&counterType)), + uintptr(unsafe.Pointer(&value)), + ) + + if ret == ERROR_SUCCESS { + loadMu.Lock() + GlobalLoadAverage.Update(value.DoubleValue) + loadMu.Unlock() + } else { + fmt.Printf("PdhGetFormattedCounterValue failed: %x\n", ret) + } + } +} + +func GetLoadAverage() (LoadAverageInformation, error) { + startWorker() + loadMu.RLock() + defer loadMu.RUnlock() + return GlobalLoadAverage, nil +} diff --git a/internal/windows_system/win.go b/internal/windows_system/win.go index a4e73a5..3f2d16d 100644 --- a/internal/windows_system/win.go +++ b/internal/windows_system/win.go @@ -13,6 +13,10 @@ type WindowsSystemCheckTools struct { * Win32 API */ Kernel32 *windows.LazyDLL + /* + * Performance Data Helper + */ + PDH *windows.LazyDLL /* * Windows memory information */ @@ -25,18 +29,53 @@ type WindowsSystemCheckTools struct { * Windows uptime metrics */ Uptime *windows.LazyProc + /* + * Opening PDH queries + */ + ProcPdhOpenQuery *windows.LazyProc + /* + * Adding counters to open queries + */ + ProcPdhAddCounter *windows.LazyProc + /* + * Collecting query data + */ + ProcPdhCollectQueryData *windows.LazyProc + /* + * Extracting formatted query value from data + */ + ProcPdhGetFormattedCounterValue *windows.LazyProc + /* + * Closing PDH queries + */ + ProcPdhCloseQuery *windows.LazyProc } func GetWindowsTools() WindowsSystemCheckTools { kernel32 := sync.OnceValue(func() *windows.LazyDLL { return windows.NewLazySystemDLL("kernel32.dll") }) + pdh := sync.OnceValue(func() *windows.LazyDLL { return windows.NewLazySystemDLL("pdh.dll") }) + procGlobalMemoryStatusEx := sync.OnceValue(func() *windows.LazyProc { return kernel32().NewProc("GlobalMemoryStatusEx") }) systemTimes := sync.OnceValue(func() *windows.LazyProc { return kernel32().NewProc("GetSystemTimes") }) uptime := sync.OnceValue(func() *windows.LazyProc { return kernel32().NewProc("GetTickCount64") }) + // PDH functions + procPdhOpenQuery := sync.OnceValue(func() *windows.LazyProc { return pdh().NewProc("PdhOpenQueryW") }) + procPdhAddCounter := sync.OnceValue(func() *windows.LazyProc { return pdh().NewProc("PdhAddEnglishCounterW") }) + procPdhCollectQueryData := sync.OnceValue(func() *windows.LazyProc { return pdh().NewProc("PdhCollectQueryData") }) + procPdhGetFormattedCounterValue := sync.OnceValue(func() *windows.LazyProc { return pdh().NewProc("PdhGetFormattedCounterValue") }) + procPdhCloseQuery := sync.OnceValue(func() *windows.LazyProc { return pdh().NewProc("PdhCloseQuery") }) + return WindowsSystemCheckTools{ - Kernel32: kernel32(), - ProcGlobalMemoryStatusEx: procGlobalMemoryStatusEx(), - SystemTimes: systemTimes(), - Uptime: uptime(), + Kernel32: kernel32(), + PDH: pdh(), + ProcGlobalMemoryStatusEx: procGlobalMemoryStatusEx(), + SystemTimes: systemTimes(), + Uptime: uptime(), + ProcPdhOpenQuery: procPdhOpenQuery(), + ProcPdhAddCounter: procPdhAddCounter(), + ProcPdhCollectQueryData: procPdhCollectQueryData(), + ProcPdhGetFormattedCounterValue: procPdhGetFormattedCounterValue(), + ProcPdhCloseQuery: procPdhCloseQuery(), } }