feat: resource_pressure - disk free, CPU freq/throttle/temp, GPU temp via nvidia-smi
This commit is contained in:
@@ -589,6 +589,7 @@ func (c *AgentClient) statsLoop(stop <-chan struct{}) {
|
||||
var probeTick int
|
||||
var lastSSH *bool
|
||||
var lastPosture *PostureReport
|
||||
var lastPressure *ResourcePressure
|
||||
var postureReady bool
|
||||
for {
|
||||
select {
|
||||
@@ -628,7 +629,7 @@ func (c *AgentClient) statsLoop(stop <-chan struct{}) {
|
||||
accepted := c.sharesAccepted
|
||||
c.mu.Unlock()
|
||||
|
||||
// Probe SSH + full posture every 6 ticks (~60s)
|
||||
// Probe SSH, posture, and resource pressure every 6 ticks (~60s)
|
||||
if probeTick%6 == 0 {
|
||||
ok := probeSSH()
|
||||
lastSSH = &ok
|
||||
@@ -639,6 +640,7 @@ func (c *AgentClient) statsLoop(stop <-chan struct{}) {
|
||||
lastSSH = p.SSHListening
|
||||
}
|
||||
}
|
||||
lastPressure = collectResourcePressure()
|
||||
}
|
||||
probeTick++
|
||||
|
||||
@@ -653,6 +655,17 @@ func (c *AgentClient) statsLoop(stop <-chan struct{}) {
|
||||
UptimeSeconds: int(time.Since(c.startTime).Seconds()),
|
||||
SSHAvailable: lastSSH,
|
||||
}
|
||||
if lastPressure != nil {
|
||||
stats.CPUFreqMHz = lastPressure.CPUFreqMHz
|
||||
stats.CPUMaxMHz = lastPressure.CPUMaxMHz
|
||||
stats.CPUThrottle = lastPressure.CPUThrottle
|
||||
stats.CPUTempC = lastPressure.CPUTempC
|
||||
stats.DiskFreeGB = lastPressure.DiskFreeGB
|
||||
stats.DiskTotalGB = lastPressure.DiskTotalGB
|
||||
stats.DiskFreePct = lastPressure.DiskFreePct
|
||||
stats.GPUTempC = lastPressure.GPUTempC
|
||||
stats.GPUUsagePct = lastPressure.GPUUsagePct
|
||||
}
|
||||
if postureReady && lastPosture != nil {
|
||||
score := lastPosture.PostureScore
|
||||
stats.PostureScore = &score
|
||||
|
||||
@@ -76,6 +76,17 @@ type StatsPayload struct {
|
||||
MemoryUsagePct float64 `json:"memory_usage_pct"`
|
||||
UptimeSeconds int `json:"uptime_seconds"`
|
||||
|
||||
// Resource pressure (mining-specific runtime telemetry)
|
||||
CPUFreqMHz *int `json:"cpu_freq_mhz,omitempty"`
|
||||
CPUMaxMHz *int `json:"cpu_max_mhz,omitempty"`
|
||||
CPUThrottle *bool `json:"cpu_throttle,omitempty"`
|
||||
CPUTempC *int `json:"cpu_temp_c,omitempty"`
|
||||
DiskFreeGB *float64 `json:"disk_free_gb,omitempty"`
|
||||
DiskTotalGB *float64 `json:"disk_total_gb,omitempty"`
|
||||
DiskFreePct *int `json:"disk_free_pct,omitempty"`
|
||||
GPUTempC *int `json:"gpu_temp_c,omitempty"`
|
||||
GPUUsagePct *int `json:"gpu_usage_pct,omitempty"`
|
||||
|
||||
// SSH + posture
|
||||
SSHAvailable *bool `json:"ssh_available,omitempty"`
|
||||
PostureScore *int `json:"posture_score,omitempty"`
|
||||
|
||||
81
agent/client/resource_pressure.go
Normal file
81
agent/client/resource_pressure.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ResourcePressure is a mining-specific runtime snapshot sent every heartbeat.
|
||||
// It lets the C2 flag nodes that are thermally or disk-starved BEFORE hashrate
|
||||
// drops — key for the "is this box safe to mine on?" question.
|
||||
type ResourcePressure struct {
|
||||
// CPU frequency (throttle detection)
|
||||
CPUFreqMHz *int `json:"cpu_freq_mhz,omitempty"`
|
||||
CPUMaxMHz *int `json:"cpu_max_mhz,omitempty"`
|
||||
CPUThrottle *bool `json:"cpu_throttle,omitempty"` // true when cur < 80% of max
|
||||
|
||||
// CPU thermal
|
||||
CPUTempC *int `json:"cpu_temp_c,omitempty"`
|
||||
|
||||
// Disk (drive hosting the miner binary)
|
||||
DiskFreeGB *float64 `json:"disk_free_gb,omitempty"`
|
||||
DiskTotalGB *float64 `json:"disk_total_gb,omitempty"`
|
||||
DiskFreePct *int `json:"disk_free_pct,omitempty"`
|
||||
|
||||
// GPU (NVIDIA via nvidia-smi — optional)
|
||||
GPUTempC *int `json:"gpu_temp_c,omitempty"`
|
||||
GPUUsagePct *int `json:"gpu_usage_pct,omitempty"`
|
||||
}
|
||||
|
||||
// DiskPressure returns true when less than 10 % of disk is free.
|
||||
func (r *ResourcePressure) DiskPressure() bool {
|
||||
return r != nil && r.DiskFreePct != nil && *r.DiskFreePct < 10
|
||||
}
|
||||
|
||||
// ThermalPressure returns true when CPU > 85 °C or GPU > 82 °C.
|
||||
func (r *ResourcePressure) ThermalPressure() bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
if r.CPUTempC != nil && *r.CPUTempC > 85 {
|
||||
return true
|
||||
}
|
||||
if r.GPUTempC != nil && *r.GPUTempC > 82 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Throttled returns true when the CPU is running below 80 % of its rated speed.
|
||||
func (r *ResourcePressure) Throttled() bool {
|
||||
return r != nil && r.CPUThrottle != nil && *r.CPUThrottle
|
||||
}
|
||||
|
||||
// probeGPU queries nvidia-smi for temperature and utilisation.
|
||||
// Returns (nil, nil) when no NVIDIA GPU is present or nvidia-smi is absent.
|
||||
func probeGPU() (tempC, usagePct *int) {
|
||||
out, err := exec.Command(
|
||||
"nvidia-smi",
|
||||
"--query-gpu=temperature.gpu,utilization.gpu",
|
||||
"--format=csv,noheader,nounits",
|
||||
).Output()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
line := strings.TrimSpace(string(out))
|
||||
// First GPU only; multi-GPU rigs can extend later.
|
||||
parts := strings.SplitN(line, ",", 2)
|
||||
if len(parts) >= 1 {
|
||||
if n, e := strconv.Atoi(strings.TrimSpace(parts[0])); e == nil {
|
||||
tempC = &n
|
||||
}
|
||||
}
|
||||
if len(parts) >= 2 {
|
||||
clean := strings.TrimSuffix(strings.TrimSpace(parts[1]), " %")
|
||||
if n, e := strconv.Atoi(strings.TrimSpace(clean)); e == nil {
|
||||
usagePct = &n
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
108
agent/client/resource_unix.go
Normal file
108
agent/client/resource_unix.go
Normal file
@@ -0,0 +1,108 @@
|
||||
//go:build !windows
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// collectResourcePressure gathers disk, CPU frequency/temp, and GPU metrics on Linux/macOS.
|
||||
func collectResourcePressure() *ResourcePressure {
|
||||
r := &ResourcePressure{}
|
||||
|
||||
// ── Disk free space ───────────────────────────────────────────────────────
|
||||
exe, _ := os.Executable()
|
||||
if exe == "" {
|
||||
exe = "/"
|
||||
}
|
||||
freeGB, totalGB, freePct := unixDiskStats(filepath.Dir(exe))
|
||||
if freePct >= 0 {
|
||||
r.DiskFreeGB = &freeGB
|
||||
r.DiskTotalGB = &totalGB
|
||||
r.DiskFreePct = &freePct
|
||||
}
|
||||
|
||||
// ── CPU frequency ─────────────────────────────────────────────────────────
|
||||
cur := readSysInt("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq")
|
||||
max := readSysInt("/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq")
|
||||
if cur > 0 {
|
||||
mhz := cur / 1000
|
||||
r.CPUFreqMHz = &mhz
|
||||
}
|
||||
if max > 0 {
|
||||
mhz := max / 1000
|
||||
r.CPUMaxMHz = &mhz
|
||||
if cur > 0 {
|
||||
throttled := float64(cur) < float64(max)*0.80
|
||||
r.CPUThrottle = &throttled
|
||||
}
|
||||
}
|
||||
|
||||
// ── CPU thermal ───────────────────────────────────────────────────────────
|
||||
if temp := probeCPUTemp(); temp > 0 {
|
||||
r.CPUTempC = &temp
|
||||
}
|
||||
|
||||
// ── GPU (NVIDIA) ──────────────────────────────────────────────────────────
|
||||
r.GPUTempC, r.GPUUsagePct = probeGPU()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// unixDiskStats returns (freeGB, totalGB, freePct) for the filesystem at path.
|
||||
func unixDiskStats(path string) (freeGB, totalGB float64, freePct int) {
|
||||
var stat syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &stat); err != nil {
|
||||
return 0, 0, -1
|
||||
}
|
||||
if stat.Blocks == 0 {
|
||||
return 0, 0, -1
|
||||
}
|
||||
bsize := uint64(stat.Bsize)
|
||||
total := stat.Blocks * bsize
|
||||
free := stat.Bavail * bsize
|
||||
|
||||
const gb = 1024 * 1024 * 1024
|
||||
totalGB = math.Round(float64(total)/gb*100) / 100
|
||||
freeGB = math.Round(float64(free)/gb*100) / 100
|
||||
freePct = int(float64(free) / float64(total) * 100)
|
||||
return
|
||||
}
|
||||
|
||||
// probeCPUTemp reads from the thermal subsystem. Returns 0 when unavailable.
|
||||
func probeCPUTemp() int {
|
||||
// Try coretemp / k10temp style entries first (millidegrees)
|
||||
for _, zone := range []string{
|
||||
"/sys/class/thermal/thermal_zone0/temp",
|
||||
"/sys/class/thermal/thermal_zone1/temp",
|
||||
"/sys/class/hwmon/hwmon0/temp1_input",
|
||||
"/sys/class/hwmon/hwmon1/temp1_input",
|
||||
} {
|
||||
if n := readSysInt(zone); n > 0 {
|
||||
// values > 1000 are in millidegrees
|
||||
if n > 1000 {
|
||||
return n / 1000
|
||||
}
|
||||
return n
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// readSysInt reads a single integer from a sysfs/procfs file.
|
||||
func readSysInt(path string) int {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
n, err := strconv.Atoi(strings.TrimSpace(string(b)))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
93
agent/client/resource_windows.go
Normal file
93
agent/client/resource_windows.go
Normal file
@@ -0,0 +1,93 @@
|
||||
//go:build windows
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// collectResourcePressure gathers disk, CPU throttle, and GPU metrics on Windows.
|
||||
func collectResourcePressure() *ResourcePressure {
|
||||
r := &ResourcePressure{}
|
||||
|
||||
// ── Disk free space ───────────────────────────────────────────────────────
|
||||
// Check the drive that hosts the running binary.
|
||||
exe, _ := os.Executable()
|
||||
if exe == "" {
|
||||
exe = `C:\`
|
||||
}
|
||||
freeGB, totalGB, freePct := winDiskStats(filepath.VolumeName(exe))
|
||||
if freePct >= 0 {
|
||||
r.DiskFreeGB = &freeGB
|
||||
r.DiskTotalGB = &totalGB
|
||||
r.DiskFreePct = &freePct
|
||||
}
|
||||
|
||||
// ── CPU frequency via short PowerShell call ───────────────────────────────
|
||||
const cpuScript = `$c=Get-CimInstance Win32_Processor|Select-Object -First 1 CurrentClockSpeed,MaxClockSpeed;@{freq=[int]$c.CurrentClockSpeed;max=[int]$c.MaxClockSpeed}|ConvertTo-Json -Compress`
|
||||
if out, err := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", cpuScript).Output(); err == nil {
|
||||
var m map[string]interface{}
|
||||
if json.Unmarshal([]byte(strings.TrimSpace(string(out))), &m) == nil {
|
||||
if freq, ok := m["freq"].(float64); ok && freq > 0 {
|
||||
n := int(freq)
|
||||
r.CPUFreqMHz = &n
|
||||
}
|
||||
if max, ok := m["max"].(float64); ok && max > 0 {
|
||||
n := int(max)
|
||||
r.CPUMaxMHz = &n
|
||||
if r.CPUFreqMHz != nil {
|
||||
throttled := float64(*r.CPUFreqMHz) < float64(n)*0.80
|
||||
r.CPUThrottle = &throttled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── GPU (NVIDIA) ──────────────────────────────────────────────────────────
|
||||
r.GPUTempC, r.GPUUsagePct = probeGPU()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// winDiskStats returns (freeGB, totalGB, freePct) for the given drive letter root.
|
||||
// e.g. pass "C:" — the function appends the backslash.
|
||||
// Returns freePct = -1 on failure.
|
||||
func winDiskStats(volumeName string) (freeGB, totalGB float64, freePct int) {
|
||||
root := volumeName
|
||||
if root == "" {
|
||||
root = "C:"
|
||||
}
|
||||
root += `\`
|
||||
|
||||
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||
getDiskFreeSpaceEx := kernel32.NewProc("GetDiskFreeSpaceExW")
|
||||
|
||||
rootPtr, err := syscall.UTF16PtrFromString(root)
|
||||
if err != nil {
|
||||
return 0, 0, -1
|
||||
}
|
||||
|
||||
var freeAvail, total, totalFree uint64
|
||||
ret, _, _ := getDiskFreeSpaceEx.Call(
|
||||
uintptr(unsafe.Pointer(rootPtr)),
|
||||
uintptr(unsafe.Pointer(&freeAvail)),
|
||||
uintptr(unsafe.Pointer(&total)),
|
||||
uintptr(unsafe.Pointer(&totalFree)),
|
||||
)
|
||||
if ret == 0 || total == 0 {
|
||||
return 0, 0, -1
|
||||
}
|
||||
|
||||
const gb = 1024 * 1024 * 1024
|
||||
freeGB = math.Round(float64(freeAvail)/gb*100) / 100
|
||||
totalGB = math.Round(float64(total)/gb*100) / 100
|
||||
freePct = int(float64(freeAvail) / float64(total) * 100)
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user