modularize monitor, reduce init usage (#81)

* kill process tree using syscall on windows & cleanup (#80)

* kill process tree using syscall on windows & cleanup

* use job api

* add error check for cmd.Start

* modularize monitor, reduce init usage

* replace slices with sort

* update gopsutil & other dependencies
This commit is contained in:
UUBulb
2024-11-03 21:53:09 +08:00
committed by GitHub
parent 0cba96bae1
commit af41e4d843
23 changed files with 665 additions and 381 deletions
+247
View File
@@ -0,0 +1,247 @@
//go:build darwin
package gpu
import (
"context"
"fmt"
"unsafe"
"github.com/ebitengine/purego"
"github.com/nezhahq/agent/pkg/util"
)
type (
CFStringEncoding = uint32
CFIndex = int32
CFTypeID = int32
CFNumberType = CFIndex
CFTypeRef = unsafe.Pointer
CFStringRef = unsafe.Pointer
CFDictionaryRef = unsafe.Pointer
machPort = uint32
ioIterator = uint32
ioObject = uint32
ioRegistryEntry = uint32
ioService = uint32
IOOptionBits = uint32
)
type (
CFStringCreateWithCStringFunc = func(alloc uintptr, cStr string, encoding CFStringEncoding) CFStringRef
CFGetTypeIDFunc = func(cf uintptr) CFTypeID
CFStringGetTypeIDFunc = func() CFTypeID
CFStringGetLengthFunc = func(theString uintptr) int32
CFStringGetCStringFunc = func(cfStr uintptr, buffer *byte, size CFIndex, encoding CFStringEncoding) bool
CFDictionaryGetTypeIDFunc = func() CFTypeID
CFDictionaryGetValueFunc = func(dict, key uintptr) unsafe.Pointer
CFDataGetTypeIDFunc = func() CFTypeID
CFDataGetBytePtrFunc = func(theData uintptr) unsafe.Pointer
CFDataGetLengthFunc = func(theData uintptr) CFIndex
CFNumberGetValueFunc = func(number uintptr, theType CFNumberType, valuePtr uintptr) bool
CFReleaseFunc = func(cf uintptr)
IOServiceGetMatchingServicesFunc = func(mainPort machPort, matching uintptr, existing *ioIterator) ioService
IOIteratorNextFunc = func(iterator ioIterator) ioObject
IOServiceMatchingFunc = func(name string) CFDictionaryRef
IORegistryEntrySearchCFPropertyFunc = func(entry ioRegistryEntry, plane string, key, allocator uintptr, options IOOptionBits) CFTypeRef
IOObjectReleaseFunc = func(object ioObject) int
)
const (
KERN_SUCCESS = 0
MACH_PORT_NULL = 0
IOSERVICE_GPU = "IOAccelerator"
IOSERVICE_PCI = "IOPCIDevice"
kIOServicePlane = "IOService"
kIORegistryIterateRecursively = 1
kCFStringEncodingUTF8 = 0x08000100
kCFNumberIntType = 9
)
var (
kCFAllocatorDefault uintptr = 0
kIOMainPortDefault machPort = 0
)
var (
coreFoundation, _ = purego.Dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", purego.RTLD_LAZY|purego.RTLD_GLOBAL)
ioKit, _ = purego.Dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", purego.RTLD_LAZY|purego.RTLD_GLOBAL)
)
var (
CFStringCreateWithCString CFStringCreateWithCStringFunc
CFGetTypeID CFGetTypeIDFunc
CFStringGetTypeID CFStringGetTypeIDFunc
CFStringGetLength CFStringGetLengthFunc
CFStringGetCString CFStringGetCStringFunc
CFDictionaryGetTypeID CFDictionaryGetTypeIDFunc
CFDictionaryGetValue CFDictionaryGetValueFunc
CFDataGetTypeID CFDataGetTypeIDFunc
CFDataGetBytePtr CFDataGetBytePtrFunc
CFDataGetLength CFDataGetLengthFunc
CFNumberGetValue CFNumberGetValueFunc
CFRelease CFReleaseFunc
IOServiceGetMatchingServices IOServiceGetMatchingServicesFunc
IOIteratorNext IOIteratorNextFunc
IOServiceMatching IOServiceMatchingFunc
IORegistryEntrySearchCFProperty IORegistryEntrySearchCFPropertyFunc
IOObjectRelease IOObjectReleaseFunc
)
var validVendors = []string{
"AMD", "Intel", "NVIDIA", "Apple",
}
func init() {
purego.RegisterLibFunc(&CFStringCreateWithCString, coreFoundation, "CFStringCreateWithCString")
purego.RegisterLibFunc(&CFGetTypeID, coreFoundation, "CFGetTypeID")
purego.RegisterLibFunc(&CFStringGetTypeID, coreFoundation, "CFStringGetTypeID")
purego.RegisterLibFunc(&CFStringGetLength, coreFoundation, "CFStringGetLength")
purego.RegisterLibFunc(&CFStringGetCString, coreFoundation, "CFStringGetCString")
purego.RegisterLibFunc(&CFDictionaryGetTypeID, coreFoundation, "CFDictionaryGetTypeID")
purego.RegisterLibFunc(&CFDictionaryGetValue, coreFoundation, "CFDictionaryGetValue")
purego.RegisterLibFunc(&CFDataGetTypeID, coreFoundation, "CFDataGetTypeID")
purego.RegisterLibFunc(&CFDataGetBytePtr, coreFoundation, "CFDataGetBytePtr")
purego.RegisterLibFunc(&CFDataGetLength, coreFoundation, "CFDataGetLength")
purego.RegisterLibFunc(&CFNumberGetValue, coreFoundation, "CFNumberGetValue")
purego.RegisterLibFunc(&CFRelease, coreFoundation, "CFRelease")
purego.RegisterLibFunc(&IOServiceGetMatchingServices, ioKit, "IOServiceGetMatchingServices")
purego.RegisterLibFunc(&IOIteratorNext, ioKit, "IOIteratorNext")
purego.RegisterLibFunc(&IOServiceMatching, ioKit, "IOServiceMatching")
purego.RegisterLibFunc(&IORegistryEntrySearchCFProperty, ioKit, "IORegistryEntrySearchCFProperty")
purego.RegisterLibFunc(&IOObjectRelease, ioKit, "IOObjectRelease")
}
func GetHost(_ context.Context) ([]string, error) {
models, err := findDevices("model")
if err != nil {
return nil, err
}
return util.RemoveDuplicate(models), nil
}
func GetState(_ context.Context) ([]float64, error) {
usage, err := findUtilization("PerformanceStatistics", "Device Utilization %")
return []float64{float64(usage)}, err
}
func findDevices(key string) ([]string, error) {
var iterator ioIterator
var results []string
done := false
iv := IOServiceGetMatchingServices(kIOMainPortDefault, uintptr(IOServiceMatching(IOSERVICE_GPU)), &iterator)
if iv != KERN_SUCCESS {
return nil, fmt.Errorf("error retrieving GPU entry")
}
var service ioObject
index := 0
for {
service = IOIteratorNext(iterator)
if service == MACH_PORT_NULL {
break
}
cfStr := CFStringCreateWithCString(kCFAllocatorDefault, key, kCFStringEncodingUTF8)
result, _, _ := findProperties(service, uintptr(cfStr), 0)
IOObjectRelease(service)
if util.ContainsStr(validVendors, result) {
results = append(results, result)
index++
} else if key == "model" && !done {
IOObjectRelease(iterator)
iv = IOServiceGetMatchingServices(kIOMainPortDefault, uintptr(IOServiceMatching(IOSERVICE_PCI)), &iterator)
if iv != KERN_SUCCESS {
return nil, fmt.Errorf("error retrieving GPU entry")
}
done = true
}
}
IOObjectRelease(iterator)
return results, nil
}
func findUtilization(key, dictKey string) (int, error) {
var iterator ioIterator
var err error
result := 0
iv := IOServiceGetMatchingServices(kIOMainPortDefault, uintptr(IOServiceMatching(IOSERVICE_GPU)), &iterator)
if iv != KERN_SUCCESS {
return 0, fmt.Errorf("error retrieving GPU entry")
}
// Only retrieving the utilization of a single GPU here
var service ioObject
for {
service = IOIteratorNext(iterator)
if service == MACH_PORT_NULL {
break
}
cfStr := CFStringCreateWithCString(kCFAllocatorDefault, key, CFStringEncoding(kCFStringEncodingUTF8))
cfDictStr := CFStringCreateWithCString(kCFAllocatorDefault, dictKey, CFStringEncoding(kCFStringEncodingUTF8))
_, result, err = findProperties(service, uintptr(cfStr), uintptr(cfDictStr))
CFRelease(uintptr(cfStr))
CFRelease(uintptr(cfDictStr))
if err != nil {
IOObjectRelease(service)
continue
} else if result != 0 {
break
}
}
IOObjectRelease(service)
IOObjectRelease(iterator)
return result, err
}
func findProperties(service ioRegistryEntry, key, dictKey uintptr) (string, int, error) {
properties := IORegistryEntrySearchCFProperty(service, kIOServicePlane, key, kCFAllocatorDefault, kIORegistryIterateRecursively)
ptrValue := uintptr(properties)
if properties != nil {
switch CFGetTypeID(ptrValue) {
// model
case CFStringGetTypeID():
length := CFStringGetLength(ptrValue) + 1 // null terminator
buf := make([]byte, length-1)
CFStringGetCString(ptrValue, &buf[0], length, uint32(kCFStringEncodingUTF8))
CFRelease(ptrValue)
return string(buf), 0, nil
case CFDataGetTypeID():
length := CFDataGetLength(ptrValue)
bin := unsafe.String((*byte)(CFDataGetBytePtr(ptrValue)), length)
CFRelease(ptrValue)
return bin, 0, nil
// PerformanceStatistics
case CFDictionaryGetTypeID():
cfValue := CFDictionaryGetValue(ptrValue, dictKey)
if cfValue != nil {
var value int
if CFNumberGetValue(uintptr(cfValue), kCFNumberIntType, uintptr(unsafe.Pointer(&value))) {
return "", value, nil
} else {
return "", 0, fmt.Errorf("failed to exec CFNumberGetValue")
}
} else {
return "", 0, fmt.Errorf("failed to exec CFDictionaryGetValue")
}
}
}
return "", 0, fmt.Errorf("failed to exec IORegistryEntrySearchCFProperty")
}
+13
View File
@@ -0,0 +1,13 @@
//go:build !darwin && !linux && !windows
package gpu
import "context"
func GetHost(_ context.Context) ([]string, error) {
return nil, nil
}
func GetState(_ context.Context) ([]float64, error) {
return nil, nil
}
+126
View File
@@ -0,0 +1,126 @@
//go:build linux
package gpu
import (
"context"
"errors"
"github.com/nezhahq/agent/pkg/monitor/gpu/vendor"
)
const (
vendorAMD = iota + 1
vendorNVIDIA
)
var vendorType = getVendor()
func getVendor() uint8 {
_, err := getNvidiaStat()
if err != nil {
return vendorAMD
} else {
return vendorNVIDIA
}
}
func getNvidiaStat() ([]float64, error) {
smi := &vendor.NvidiaSMI{
BinPath: "/usr/bin/nvidia-smi",
}
err1 := smi.Start()
if err1 != nil {
return nil, err1
}
data, err2 := smi.GatherUsage()
if err2 != nil {
return nil, err2
}
return data, nil
}
func getAMDStat() ([]float64, error) {
rsmi := &vendor.ROCmSMI{
BinPath: "/opt/rocm/bin/rocm-smi",
}
err := rsmi.Start()
if err != nil {
return nil, err
}
data, err := rsmi.GatherUsage()
if err != nil {
return nil, err
}
return data, nil
}
func getNvidiaHost() ([]string, error) {
smi := &vendor.NvidiaSMI{
BinPath: "/usr/bin/nvidia-smi",
}
err := smi.Start()
if err != nil {
return nil, err
}
data, err := smi.GatherModel()
if err != nil {
return nil, err
}
return data, nil
}
func getAMDHost() ([]string, error) {
rsmi := &vendor.ROCmSMI{
BinPath: "/opt/rocm/bin/rocm-smi",
}
err := rsmi.Start()
if err != nil {
return nil, err
}
data, err := rsmi.GatherModel()
if err != nil {
return nil, err
}
return data, nil
}
func GetHost(_ context.Context) ([]string, error) {
var gi []string
var err error
switch vendorType {
case vendorAMD:
gi, err = getAMDHost()
case vendorNVIDIA:
gi, err = getNvidiaHost()
default:
return nil, errors.New("invalid vendor")
}
if err != nil {
return nil, err
}
return gi, nil
}
func GetState(_ context.Context) ([]float64, error) {
var gs []float64
var err error
switch vendorType {
case vendorAMD:
gs, err = getAMDStat()
case vendorNVIDIA:
gs, err = getNvidiaStat()
default:
return nil, errors.New("invalid vendor")
}
if err != nil {
return nil, err
}
return gs, nil
}
+170
View File
@@ -0,0 +1,170 @@
//go:build windows
package gpu
import (
"context"
"errors"
"fmt"
"time"
"unsafe"
"github.com/jaypipes/ghw"
"golang.org/x/sys/windows"
)
const (
ERROR_SUCCESS = 0
PDH_FMT_DOUBLE = 0x00000200
PDH_MORE_DATA = 0x800007d2
PDH_VAILD_DATA = 0x00000000
PDH_NEW_DATA = 0x00000001
PDH_NO_DATA = 0x800007d5
)
var (
modPdh = windows.NewLazySystemDLL("pdh.dll")
pdhOpenQuery = modPdh.NewProc("PdhOpenQuery")
pdhCollectQueryData = modPdh.NewProc("PdhCollectQueryData")
pdhGetFormattedCounterArrayW = modPdh.NewProc("PdhGetFormattedCounterArrayW")
pdhAddEnglishCounterW = modPdh.NewProc("PdhAddEnglishCounterW")
pdhCloseQuery = modPdh.NewProc("PdhCloseQuery")
)
type PDH_FMT_COUNTERVALUE_DOUBLE struct {
CStatus uint32
DoubleValue float64
}
type PDH_FMT_COUNTERVALUE_ITEM_DOUBLE struct {
SzName *uint16
FmtValue PDH_FMT_COUNTERVALUE_DOUBLE
}
func GetHost(_ context.Context) ([]string, error) {
var gpuModel []string
gi, err := ghw.GPU(ghw.WithDisableWarnings())
if err != nil {
return nil, err
}
for _, card := range gi.GraphicsCards {
if card.DeviceInfo == nil {
return nil, errors.New("Cannot find device info")
}
gpuModel = append(gpuModel, card.DeviceInfo.Product.Name)
}
return gpuModel, nil
}
func GetState(_ context.Context) ([]float64, error) {
counter, err := newWin32PerformanceCounter("gpu_utilization", "\\GPU Engine(*engtype_3D)\\Utilization Percentage")
if err != nil {
return nil, err
}
defer pdhCloseQuery.Call(uintptr(counter.Query))
values, err := getValue(8192, counter)
if err != nil {
return nil, err
}
tot := sumArray(values)
if tot > 100 {
tot = 100
}
return []float64{tot}, nil
}
// https://github.com/influxdata/telegraf/blob/master/plugins/inputs/win_perf_counters/performance_query.go
func getCounterArrayValue(initialBufSize uint32, counter *win32PerformanceCounter) ([]float64, error) {
for buflen := initialBufSize; buflen <= 100*1024*1024; buflen *= 2 {
time.Sleep(10 * time.Millisecond) // GPU 查询必须设置间隔,否则数据不准
s, _, err := pdhCollectQueryData.Call(uintptr(counter.Query))
if s != 0 && err != nil {
if s == PDH_NO_DATA {
return nil, fmt.Errorf("%w: this counter has not data", err)
}
return nil, err
}
buf := make([]byte, buflen)
size := buflen
var itemCount uint32
r, _, _ := pdhGetFormattedCounterArrayW.Call(uintptr(counter.Counter), PDH_FMT_DOUBLE, uintptr(unsafe.Pointer(&size)), uintptr(unsafe.Pointer(&itemCount)), uintptr(unsafe.Pointer(&buf[0])))
if r == ERROR_SUCCESS {
items := (*[1 << 20]PDH_FMT_COUNTERVALUE_ITEM_DOUBLE)(unsafe.Pointer(&buf[0]))[:itemCount:itemCount]
values := make([]float64, 0, itemCount)
for _, item := range items {
if item.FmtValue.CStatus == PDH_VAILD_DATA || item.FmtValue.CStatus == PDH_NEW_DATA {
val := item.FmtValue.DoubleValue
values = append(values, val)
}
}
return values, nil
}
if r != PDH_MORE_DATA {
return nil, fmt.Errorf("pdhGetFormattedCounterArrayW failed with status 0x%X", r)
}
}
return nil, errors.New("buffer limit reached")
}
func createQuery() (windows.Handle, error) {
var query windows.Handle
r, _, err := pdhOpenQuery.Call(0, 0, uintptr(unsafe.Pointer(&query)))
if r != ERROR_SUCCESS {
return 0, fmt.Errorf("pdhOpenQuery failed with status 0x%X: %v", r, err)
}
return query, nil
}
type win32PerformanceCounter struct {
PostName string
CounterName string
Query windows.Handle
Counter windows.Handle
}
func newWin32PerformanceCounter(postName, counterName string) (*win32PerformanceCounter, error) {
query, err := createQuery()
if err != nil {
return nil, err
}
counter := win32PerformanceCounter{
Query: query,
PostName: postName,
CounterName: counterName,
}
r, _, err := pdhAddEnglishCounterW.Call(
uintptr(counter.Query),
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(counter.CounterName))),
0,
uintptr(unsafe.Pointer(&counter.Counter)),
)
if r != ERROR_SUCCESS {
return nil, fmt.Errorf("pdhAddEnglishCounterW failed with status 0x%X: %v", r, err)
}
return &counter, nil
}
func getValue(initialBufSize uint32, counter *win32PerformanceCounter) ([]float64, error) {
s, _, err := pdhCollectQueryData.Call(uintptr(counter.Query))
if s != 0 && err != nil {
if s == PDH_NO_DATA {
return nil, fmt.Errorf("%w: this counter has not data", err)
}
return nil, err
}
return getCounterArrayValue(initialBufSize, counter)
}
func sumArray(arr []float64) float64 {
var sum float64
for _, value := range arr {
sum += value
}
return sum
}
+107
View File
@@ -0,0 +1,107 @@
package vendor
// Modified from https://github.com/influxdata/telegraf/blob/master/plugins/inputs/amd_rocm_smi/amd_rocm_smi.go
// Original License: MIT
import (
"errors"
"os"
"os/exec"
"github.com/tidwall/gjson"
)
type ROCmSMI struct {
BinPath string
data []byte
}
func (rsmi *ROCmSMI) GatherModel() ([]string, error) {
return rsmi.gatherModel()
}
func (rsmi *ROCmSMI) GatherUsage() ([]float64, error) {
return rsmi.gatherUsage()
}
func (rsmi *ROCmSMI) Start() error {
if _, err := os.Stat(rsmi.BinPath); os.IsNotExist(err) {
binPath, err := exec.LookPath("rocm-smi")
if err != nil {
return errors.New("didn't find the adequate tool to query GPU utilization")
}
rsmi.BinPath = binPath
}
rsmi.data = rsmi.pollROCmSMI()
return nil
}
func (rsmi *ROCmSMI) pollROCmSMI() []byte {
cmd := exec.Command(rsmi.BinPath,
"-u",
"--showproductname",
"--json",
)
gs, err := cmd.CombinedOutput()
if err != nil {
return nil
}
return gs
}
func (rsmi *ROCmSMI) gatherModel() ([]string, error) {
m, err := parseModel(rsmi.data)
if err != nil {
return nil, err
}
return m, nil
}
func (rsmi *ROCmSMI) gatherUsage() ([]float64, error) {
u, err := parseUsage(rsmi.data)
if err != nil {
return nil, err
}
return u, nil
}
func parseModel(jsonObject []byte) ([]string, error) {
if jsonObject == nil {
return nil, nil
}
result := gjson.ParseBytes(jsonObject)
if !result.IsObject() {
return nil, errors.New("invalid JSON")
}
ret := make([]string, 0)
result.ForEach(func(_, value gjson.Result) bool {
ret = append(ret, value.Get("Card series").String())
return true
})
return ret, nil
}
func parseUsage(jsonObject []byte) ([]float64, error) {
if jsonObject == nil {
return nil, nil
}
result := gjson.ParseBytes(jsonObject)
if !result.IsObject() {
return nil, errors.New("invalid JSON")
}
ret := make([]float64, 0)
result.ForEach(func(_, value gjson.Result) bool {
ret = append(ret, value.Get("GPU use (%)").Float())
return true
})
return ret, nil
}
+106
View File
@@ -0,0 +1,106 @@
package vendor
// Modified from https://github.com/influxdata/telegraf/blob/master/plugins/inputs/nvidia_smi/nvidia_smi.go
// Original License: MIT
import (
"encoding/xml"
"errors"
"os"
"os/exec"
"strconv"
"strings"
)
type NvidiaSMI struct {
BinPath string
data []byte
}
func (smi *NvidiaSMI) GatherModel() ([]string, error) {
return smi.gatherModel()
}
func (smi *NvidiaSMI) GatherUsage() ([]float64, error) {
return smi.gatherUsage()
}
func (smi *NvidiaSMI) Start() error {
if _, err := os.Stat(smi.BinPath); os.IsNotExist(err) {
binPath, err := exec.LookPath("nvidia-smi")
if err != nil {
return errors.New("didn't find the adequate tool to query GPU utilization")
}
smi.BinPath = binPath
}
smi.data = smi.pollNvidiaSMI()
return nil
}
func (smi *NvidiaSMI) pollNvidiaSMI() []byte {
cmd := exec.Command(smi.BinPath,
"-q",
"-x",
)
gs, err := cmd.CombinedOutput()
if err != nil {
return nil
}
return gs
}
func (smi *NvidiaSMI) gatherModel() ([]string, error) {
var s smistat
var models []string
err := xml.Unmarshal(smi.data, &s)
if err != nil {
return nil, err
}
for _, gpu := range s.GPUs {
models = append(models, gpu.ProductName)
}
return models, nil
}
func (smi *NvidiaSMI) gatherUsage() ([]float64, error) {
var s smistat
var percentage []float64
err := xml.Unmarshal(smi.data, &s)
if err != nil {
return nil, err
}
for _, gpu := range s.GPUs {
gp, _ := parsePercentage(gpu.Utilization.GpuUtil)
percentage = append(percentage, gp)
}
return percentage, nil
}
func parsePercentage(p string) (float64, error) {
per := strings.ReplaceAll(p, " ", "")
t := strings.TrimSuffix(per, "%")
value, err := strconv.ParseFloat(t, 64)
if err != nil {
return 0, err
}
return value, nil
}
type gpu struct {
ProductName string `xml:"product_name"`
Utilization struct {
GpuUtil string `xml:"gpu_util"`
} `xml:"utilization"`
}
type smistat struct {
GPUs []gpu `xml:"gpu"`
}