commit c06d5dcdd46616988373e6d4a9b5f007634e9980 Author: Sebastiaan de Schaetzen Date: Fri Mar 14 13:49:43 2025 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..423598c --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +idlesleep + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Go workspace file +go.work + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# IDE specific files +.idea +.vscode +*.swp +*.swo \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d6d4865 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# Idle Sleep Monitor + +This program monitors system resource usage and automatically suspends the system when it detects extended periods of low resource utilization. + +## Requirements + +- Linux system with systemd +- NVIDIA GPU with drivers installed +- Go 1.21 or later +- Root privileges (required for system suspension) + +## Installation + +1. Clone this repository +2. Install dependencies: +```bash +go mod download +``` +3. Build the program: +```bash +go build +``` + +## Usage + +The program must be run as root since it needs permissions to suspend the system: + +```bash +sudo ./idlesleep +``` + +The program will monitor the following metrics over a 5-minute period: +- CPU usage across all cores (threshold: < 20%) +- GPU usage (threshold: < 20%) +- Disk I/O (threshold: < 5 MB/s) +- Network I/O (threshold: < 1 MB/s) + +If all metrics stay below their thresholds for the entire monitoring period, the system will be suspended. + +## Configuration + +The thresholds are defined as constants in `main.go`. You can modify them by editing the following values: + +```go +const ( + checkInterval = 10 * time.Second + monitoringPeriod = 5 * time.Minute + cpuThreshold = 20.0 // percentage + gpuThreshold = 20.0 // percentage + diskThreshold = 5 * 1024 * 1024 // 5 MB/s + networkThreshold = 1 * 1024 * 1024 // 1 MB/s +) +``` \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f4ae8a0 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module idlesleep + +go 1.21 + +require ( + github.com/NVIDIA/go-nvml v0.12.4-1 + github.com/shirou/gopsutil/v3 v3.24.2 +) + +require ( + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + golang.org/x/sys v0.17.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7fd6db6 --- /dev/null +++ b/go.sum @@ -0,0 +1,48 @@ +github.com/NVIDIA/go-nvml v0.12.4-1 h1:WKUvqshhWSNTfm47ETRhv0A0zJyr1ncCuHiXwoTrBEc= +github.com/NVIDIA/go-nvml v0.12.4-1/go.mod h1:8Llmj+1Rr+9VGGwZuRer5N/aCjxGuR5nPb/9ebBiIEQ= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/shirou/gopsutil/v3 v3.24.2 h1:kcR0erMbLg5/3LcInpw0X/rrPSqq4CDPyI6A6ZRC18Y= +github.com/shirou/gopsutil/v3 v3.24.2/go.mod h1:tSg/594BcA+8UdQU2XcW803GWYgdtauFFPgJCJKZlVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..dedc932 --- /dev/null +++ b/main.go @@ -0,0 +1,147 @@ +package main + +import ( + "log" + "os" + "os/exec" + "time" + + "github.com/NVIDIA/go-nvml/pkg/nvml" + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/net" +) + +const ( + checkInterval = 10 * time.Second + monitoringPeriod = 5 * time.Minute + cpuThreshold = 20.0 // percentage + gpuThreshold = 20.0 // percentage + diskThreshold = 5 * 1024 * 1024 // 5 MB/s + networkThreshold = 1 * 1024 * 1024 // 1 MB/s +) + +type ResourceUsage struct { + timestamp time.Time + cpuUsage float64 + gpuUsage float64 + diskIO uint64 + networkIO uint64 +} + +func main() { + // Check if running as root + if os.Geteuid() != 0 { + log.Fatal("This program must be run as root") + } + + // Initialize NVML for GPU monitoring + ret := nvml.Init() + if ret != nvml.SUCCESS { + log.Printf("Warning: Could not initialize NVML: %v", ret) + } + defer nvml.Shutdown() + + usageHistory := make([]ResourceUsage, 0) + ticker := time.NewTicker(checkInterval) + defer ticker.Stop() + + log.Printf("Starting idle monitoring. System will suspend when:\n") + log.Printf("- CPU usage < %.1f%%\n", cpuThreshold) + log.Printf("- GPU usage < %.1f%%\n", gpuThreshold) + log.Printf("- Disk I/O < %.1f MB/s\n", float64(diskThreshold)/(1024*1024)) + log.Printf("- Network I/O < %.1f MB/s\n", float64(networkThreshold)/(1024*1024)) + log.Printf("Over the last %v\n", monitoringPeriod) + + for range ticker.C { + usage := getCurrentUsage() + usageHistory = append(usageHistory, usage) + + // Remove entries older than monitoring period + cutoff := time.Now().Add(-monitoringPeriod) + for i, u := range usageHistory { + if u.timestamp.After(cutoff) { + usageHistory = usageHistory[i:] + break + } + } + + if len(usageHistory) > 0 && isSystemIdle(usageHistory) { + log.Println("System has been idle for the monitoring period. Suspending...") + if err := suspendSystem(); err != nil { + log.Printf("Failed to suspend system: %v", err) + } + } + } +} + +func getCurrentUsage() ResourceUsage { + usage := ResourceUsage{ + timestamp: time.Now(), + } + + // Get CPU usage + if cpuPercent, err := cpu.Percent(0, false); err == nil && len(cpuPercent) > 0 { + usage.cpuUsage = cpuPercent[0] + } + + // Get GPU usage + count, ret := nvml.DeviceGetCount() + if ret == nvml.SUCCESS && count > 0 { + device, ret := nvml.DeviceGetHandleByIndex(0) + if ret == nvml.SUCCESS { + utilization, ret := device.GetUtilizationRates() + if ret == nvml.SUCCESS { + usage.gpuUsage = float64(utilization.Gpu) + } + } + } + + // Get disk I/O + if diskStats, err := disk.IOCounters(); err == nil { + var totalIO uint64 + for _, stat := range diskStats { + totalIO += stat.ReadBytes + stat.WriteBytes + } + usage.diskIO = totalIO + } + + // Get network I/O + if netStats, err := net.IOCounters(false); err == nil && len(netStats) > 0 { + usage.networkIO = netStats[0].BytesSent + netStats[0].BytesRecv + } + + return usage +} + +func isSystemIdle(history []ResourceUsage) bool { + if len(history) < 2 { + return false + } + + var avgCPU, avgGPU float64 + samples := len(history) + + for _, usage := range history { + avgCPU += usage.cpuUsage + avgGPU += usage.gpuUsage + } + + // Calculate I/O rates using first and last samples + duration := history[samples-1].timestamp.Sub(history[0].timestamp).Seconds() + diskIORate := float64(history[samples-1].diskIO-history[0].diskIO) / duration + netIORate := float64(history[samples-1].networkIO-history[0].networkIO) / duration + + avgCPU /= float64(samples) + avgGPU /= float64(samples) + + return avgCPU < cpuThreshold && + avgGPU < gpuThreshold && + diskIORate < float64(diskThreshold) && + netIORate < float64(networkThreshold) +} + +func suspendSystem() error { + cmd := exec.Command("systemctl", "suspend") + return cmd.Run() +} \ No newline at end of file