147 lines
3.4 KiB
Go
147 lines
3.4 KiB
Go
package system
|
||
|
||
import (
|
||
"math"
|
||
"os"
|
||
"path/filepath"
|
||
"runtime"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"nadir/internal/oscmd"
|
||
)
|
||
|
||
type CPUInfo struct {
|
||
Model string `json:"model" example:"AMD Ryzen 7 7840U" doc:"CPU model name"`
|
||
LogicalCPUs int `json:"logical_cpus" example:"16" doc:"Number of logical CPUs (cores × threads)"`
|
||
MinMHz int `json:"min_mhz" example:"400" doc:"Lowest frequency the scaling governor can select"`
|
||
MaxMHz int `json:"max_mhz" example:"5137" doc:"Highest frequency (boost ceiling)"`
|
||
CurrentMHz int `json:"current_mhz" example:"3157" doc:"Peak current clock across all cores (instantaneous snapshot; 0 if cpufreq unavailable)"`
|
||
}
|
||
|
||
func cpuInfo() CPUInfo {
|
||
data, _ := os.ReadFile("/proc/cpuinfo")
|
||
c := CPUInfo{Model: cpuModel(string(data)), LogicalCPUs: runtime.NumCPU()}
|
||
c.MinMHz, c.MaxMHz, c.CurrentMHz = cpuFreqMHz("/sys/devices/system/cpu")
|
||
mhz := cpuinfoMaxMHz(string(data))
|
||
if c.Model == "" || mhz == 0 {
|
||
model, lscpuMHz := lscpuFallback()
|
||
if c.Model == "" {
|
||
c.Model = model
|
||
}
|
||
if mhz == 0 {
|
||
mhz = lscpuMHz
|
||
}
|
||
}
|
||
if mhz > 0 {
|
||
if c.CurrentMHz == 0 {
|
||
c.CurrentMHz = mhz
|
||
}
|
||
if c.MaxMHz == 0 {
|
||
c.MaxMHz = mhz
|
||
}
|
||
if c.MinMHz == 0 {
|
||
c.MinMHz = mhz
|
||
}
|
||
}
|
||
return c
|
||
}
|
||
|
||
func lscpuFallback() (model string, mhz int) {
|
||
out, err := oscmd.Run("lscpu")
|
||
if err != nil {
|
||
return "", 0
|
||
}
|
||
for line := range strings.SplitSeq(out, "\n") {
|
||
k, v, ok := strings.Cut(line, ":")
|
||
if !ok {
|
||
continue
|
||
}
|
||
k, v = strings.TrimSpace(k), strings.TrimSpace(v)
|
||
switch k {
|
||
case "Model name":
|
||
if model == "" {
|
||
model = v
|
||
}
|
||
case "BIOS Model name":
|
||
if model == "" {
|
||
model = v
|
||
}
|
||
case "CPU max MHz", "CPU MHz":
|
||
if f, err := strconv.ParseFloat(v, 64); err == nil && int(f) > mhz {
|
||
mhz = int(math.Round(f))
|
||
}
|
||
}
|
||
}
|
||
if mhz == 0 {
|
||
mhz = parseGHzSuffix(model)
|
||
}
|
||
return model, mhz
|
||
}
|
||
|
||
func parseGHzSuffix(s string) int {
|
||
i := strings.LastIndex(s, "@")
|
||
if i < 0 {
|
||
return 0
|
||
}
|
||
rest := strings.TrimSpace(s[i+1:])
|
||
rest = strings.TrimSuffix(strings.TrimSuffix(rest, "GHz"), "Ghz")
|
||
rest = strings.TrimSpace(strings.TrimSuffix(rest, "G"))
|
||
f, err := strconv.ParseFloat(strings.TrimSpace(rest), 64)
|
||
if err != nil {
|
||
return 0
|
||
}
|
||
return int(math.Round(f * 1000))
|
||
}
|
||
|
||
func cpuinfoMaxMHz(cpuinfo string) int {
|
||
var max float64
|
||
for line := range strings.SplitSeq(cpuinfo, "\n") {
|
||
k, v, ok := strings.Cut(line, ":")
|
||
if !ok || strings.TrimSpace(k) != "cpu MHz" {
|
||
continue
|
||
}
|
||
if f, err := strconv.ParseFloat(strings.TrimSpace(v), 64); err == nil && f > max {
|
||
max = f
|
||
}
|
||
}
|
||
return int(math.Round(max))
|
||
}
|
||
|
||
func cpuFreqMHz(root string) (min, max, cur int) {
|
||
min = readKHzAsMHz(filepath.Join(root, "cpu0/cpufreq/cpuinfo_min_freq"))
|
||
max = readKHzAsMHz(filepath.Join(root, "cpu0/cpufreq/cpuinfo_max_freq"))
|
||
cores, _ := filepath.Glob(filepath.Join(root, "cpu[0-9]*/cpufreq/scaling_cur_freq"))
|
||
for _, f := range cores {
|
||
if v := readKHzAsMHz(f); v > cur {
|
||
cur = v
|
||
}
|
||
}
|
||
return min, max, cur
|
||
}
|
||
|
||
func readKHzAsMHz(path string) int {
|
||
khz, err := strconv.Atoi(readTrim(path))
|
||
if err != nil {
|
||
return 0
|
||
}
|
||
return khz / 1000
|
||
}
|
||
|
||
func cpuModel(cpuinfo string) string {
|
||
var fallback string
|
||
for line := range strings.SplitSeq(cpuinfo, "\n") {
|
||
k, v, ok := strings.Cut(line, ":")
|
||
if !ok {
|
||
continue
|
||
}
|
||
switch strings.TrimSpace(k) {
|
||
case "model name":
|
||
return strings.TrimSpace(v)
|
||
case "Model":
|
||
fallback = strings.TrimSpace(v)
|
||
}
|
||
}
|
||
return fallback
|
||
}
|