Files
kandev/apps/backend/internal/system/metrics/collector_darwin.go
T
2026-06-15 18:54:16 +01:00

120 lines
3.4 KiB
Go

//go:build darwin
package metrics
/*
#include <mach/mach.h>
#include <mach/mach_host.h>
#include <mach/host_info.h>
*/
import "C"
import (
"encoding/binary"
"errors"
"fmt"
"os"
"unsafe"
"golang.org/x/sys/unix"
)
// readCPUTimes uses Mach host_statistics(HOST_CPU_LOAD_INFO). Darwin does not
// expose kern.cp_time the way BSDs do, so we go straight to Mach. The returned
// cpu_ticks array has CPU_STATE_MAX entries (USER, SYSTEM, IDLE, NICE) of
// natural_t (uint32). Wraparound is rare at 100 Hz (~497 days) and the delta
// arithmetic in calculateCPUPercent already tolerates it via uint64 promotion.
func readCPUTimes(_ *Collector) (cpuTimes, error) {
var info C.host_cpu_load_info_data_t
count := C.mach_msg_type_number_t(C.HOST_CPU_LOAD_INFO_COUNT)
rc := C.host_statistics(
C.mach_host_self(),
C.HOST_CPU_LOAD_INFO,
C.host_info_t(unsafe.Pointer(&info)),
&count,
)
if rc != C.KERN_SUCCESS {
return cpuTimes{}, fmt.Errorf("host_statistics HOST_CPU_LOAD_INFO: kern_return %d", int(rc))
}
var total, idle uint64
for i := 0; i < int(C.CPU_STATE_MAX); i++ {
v := uint64(info.cpu_ticks[i])
total += v
if i == int(C.CPU_STATE_IDLE) {
idle = v
}
}
return cpuTimes{total: total, idle: idle}, nil
}
func (c *Collector) memoryPercent() (float64, error) {
return darwinMemoryPercent()
}
func (c *Collector) cpuTempValue() (float64, error) {
return 0, errors.New("cpu temperature unavailable on darwin")
}
func (c *Collector) ioLoadValue() (float64, error) {
return darwinLoadAvg1()
}
// vm.loadavg returns struct loadavg { fixed_pt_t ldavg[3]; long fscale; }.
// On darwin amd64/arm64 long is 8 bytes; the struct is padded to 24 bytes.
func darwinLoadAvg1() (float64, error) {
raw, err := unix.SysctlRaw("vm.loadavg")
if err != nil {
return 0, fmt.Errorf("sysctl vm.loadavg: %w", err)
}
if len(raw) < 24 {
return 0, errors.New("sysctl vm.loadavg response too short")
}
ld0 := binary.LittleEndian.Uint32(raw[0:4])
fscale := binary.LittleEndian.Uint64(raw[16:24])
if fscale == 0 {
return 0, errors.New("sysctl vm.loadavg fscale is zero")
}
return float64(ld0) / float64(fscale), nil
}
// darwinMemoryPercent treats (free + inactive + speculative) pages as
// available, matching Activity Monitor's "Memory Used / Total" approximation.
// Wired, active, and compressor pages count as used.
func darwinMemoryPercent() (float64, error) {
total, err := unix.SysctlUint64("hw.memsize")
if err != nil {
return 0, fmt.Errorf("sysctl hw.memsize: %w", err)
}
if total == 0 {
return 0, errors.New("hw.memsize is zero")
}
pageSize := uint64(os.Getpagesize())
if pageSize == 0 {
return 0, errors.New("page size is zero")
}
free, inactive, speculative, err := readVMStatistics64()
if err != nil {
return 0, err
}
available := (free + inactive + speculative) * pageSize
if available > total {
available = total
}
return (1.0 - float64(available)/float64(total)) * 100.0, nil
}
func readVMStatistics64() (free, inactive, speculative uint64, err error) {
var vm C.vm_statistics64_data_t
count := C.mach_msg_type_number_t(C.HOST_VM_INFO64_COUNT)
rc := C.host_statistics64(
C.mach_host_self(),
C.HOST_VM_INFO64,
C.host_info64_t(unsafe.Pointer(&vm)),
&count,
)
if rc != C.KERN_SUCCESS {
return 0, 0, 0, fmt.Errorf("host_statistics64 HOST_VM_INFO64: kern_return %d", int(rc))
}
return uint64(vm.free_count), uint64(vm.inactive_count), uint64(vm.speculative_count), nil
}