Skip to content

Commit

Permalink
Merge pull request #1702 from uubulb/purego_darwin
Browse files Browse the repository at this point in the history
feat(cpu, mem, sensors, disk, process)(darwin): cgo-free implementations
  • Loading branch information
shirou authored Sep 26, 2024
2 parents fdc3c05 + f56b53a commit 4140cda
Show file tree
Hide file tree
Showing 30 changed files with 1,367 additions and 1,350 deletions.
105 changes: 93 additions & 12 deletions cpu/cpu_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ package cpu

import (
"context"
"fmt"
"strconv"
"strings"
"unsafe"

"github.com/shoenig/go-m1cpu"
"github.com/tklauser/go-sysconf"
"golang.org/x/sys/unix"

"github.com/shirou/gopsutil/v4/internal/common"
)

// sys/resource.h
Expand All @@ -23,6 +26,24 @@ const (
cpUStates = 5
)

// mach/machine.h
const (
cpuStateUser = 0
cpuStateSystem = 1
cpuStateIdle = 2
cpuStateNice = 3
cpuStateMax = 4
)

// mach/processor_info.h
const (
processorCpuLoadInfo = 2
)

type hostCpuLoadInfoData struct {
cpuTicks [cpuStateMax]uint32
}

// default value. from time.h
var ClocksPerSec = float64(128)

Expand All @@ -39,11 +60,17 @@ func Times(percpu bool) ([]TimesStat, error) {
}

func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) {
lib, err := common.NewLibrary(common.System)
if err != nil {
return nil, err
}
defer lib.Close()

if percpu {
return perCPUTimes()
return perCPUTimes(lib)
}

return allCPUTimes()
return allCPUTimes(lib)
}

// Returns only one CPUInfoStat on FreeBSD
Expand Down Expand Up @@ -86,15 +113,9 @@ func InfoWithContext(ctx context.Context) ([]InfoStat, error) {
c.CacheSize = int32(cacheSize)
c.VendorID, _ = unix.Sysctl("machdep.cpu.vendor")

if m1cpu.IsAppleSilicon() {
c.Mhz = float64(m1cpu.PCoreHz() / 1_000_000)
} else {
// Use the rated frequency of the CPU. This is a static value and does not
// account for low power or Turbo Boost modes.
cpuFrequency, err := unix.SysctlUint64("hw.cpufrequency")
if err == nil {
c.Mhz = float64(cpuFrequency) / 1000000.0
}
v, err := getFrequency()
if err == nil {
c.Mhz = v
}

return append(ret, c), nil
Expand All @@ -115,3 +136,63 @@ func CountsWithContext(ctx context.Context, logical bool) (int, error) {

return int(count), nil
}

func perCPUTimes(machLib *common.Library) ([]TimesStat, error) {
machHostSelf := common.GetFunc[common.MachHostSelfFunc](machLib, common.MachHostSelfSym)
machTaskSelf := common.GetFunc[common.MachTaskSelfFunc](machLib, common.MachTaskSelfSym)
hostProcessorInfo := common.GetFunc[common.HostProcessorInfoFunc](machLib, common.HostProcessorInfoSym)
vmDeallocate := common.GetFunc[common.VMDeallocateFunc](machLib, common.VMDeallocateSym)

var count, ncpu uint32
var cpuload *hostCpuLoadInfoData

status := hostProcessorInfo(machHostSelf(), processorCpuLoadInfo, &ncpu, uintptr(unsafe.Pointer(&cpuload)), &count)

if status != common.KERN_SUCCESS {
return nil, fmt.Errorf("host_processor_info error=%d", status)
}

defer vmDeallocate(machTaskSelf(), uintptr(unsafe.Pointer(cpuload)), uintptr(ncpu))

ret := []TimesStat{}
loads := unsafe.Slice(cpuload, ncpu)

for i := 0; i < int(ncpu); i++ {
c := TimesStat{
CPU: fmt.Sprintf("cpu%d", i),
User: float64(loads[i].cpuTicks[cpuStateUser]) / ClocksPerSec,
System: float64(loads[i].cpuTicks[cpuStateSystem]) / ClocksPerSec,
Nice: float64(loads[i].cpuTicks[cpuStateNice]) / ClocksPerSec,
Idle: float64(loads[i].cpuTicks[cpuStateIdle]) / ClocksPerSec,
}

ret = append(ret, c)
}

return ret, nil
}

func allCPUTimes(machLib *common.Library) ([]TimesStat, error) {
machHostSelf := common.GetFunc[common.MachHostSelfFunc](machLib, common.MachHostSelfSym)
hostStatistics := common.GetFunc[common.HostStatisticsFunc](machLib, common.HostStatisticsSym)

var cpuload hostCpuLoadInfoData
count := uint32(cpuStateMax)

status := hostStatistics(machHostSelf(), common.HOST_CPU_LOAD_INFO,
uintptr(unsafe.Pointer(&cpuload)), &count)

if status != common.KERN_SUCCESS {
return nil, fmt.Errorf("host_statistics error=%d", status)
}

c := TimesStat{
CPU: "cpu-total",
User: float64(cpuload.cpuTicks[cpuStateUser]) / ClocksPerSec,
System: float64(cpuload.cpuTicks[cpuStateSystem]) / ClocksPerSec,
Nice: float64(cpuload.cpuTicks[cpuStateNice]) / ClocksPerSec,
Idle: float64(cpuload.cpuTicks[cpuStateIdle]) / ClocksPerSec,
}

return []TimesStat{c}, nil
}
80 changes: 80 additions & 0 deletions cpu/cpu_darwin_arm64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build darwin && arm64

package cpu

import (
"encoding/binary"
"fmt"
"unsafe"

"github.com/shirou/gopsutil/v4/internal/common"
)

// https://github.com/shoenig/go-m1cpu/blob/v0.1.6/cpu.go
func getFrequency() (float64, error) {
ioKit, err := common.NewLibrary(common.IOKit)
if err != nil {
return 0, err
}
defer ioKit.Close()

coreFoundation, err := common.NewLibrary(common.CoreFoundation)
if err != nil {
return 0, err
}
defer coreFoundation.Close()

ioServiceMatching := common.GetFunc[common.IOServiceMatchingFunc](ioKit, common.IOServiceMatchingSym)
ioServiceGetMatchingServices := common.GetFunc[common.IOServiceGetMatchingServicesFunc](ioKit, common.IOServiceGetMatchingServicesSym)
ioIteratorNext := common.GetFunc[common.IOIteratorNextFunc](ioKit, common.IOIteratorNextSym)
ioRegistryEntryGetName := common.GetFunc[common.IORegistryEntryGetNameFunc](ioKit, common.IORegistryEntryGetNameSym)
ioRegistryEntryCreateCFProperty := common.GetFunc[common.IORegistryEntryCreateCFPropertyFunc](ioKit, common.IORegistryEntryCreateCFPropertySym)
ioObjectRelease := common.GetFunc[common.IOObjectReleaseFunc](ioKit, common.IOObjectReleaseSym)

cfStringCreateWithCString := common.GetFunc[common.CFStringCreateWithCStringFunc](coreFoundation, common.CFStringCreateWithCStringSym)
cfDataGetLength := common.GetFunc[common.CFDataGetLengthFunc](coreFoundation, common.CFDataGetLengthSym)
cfDataGetBytePtr := common.GetFunc[common.CFDataGetBytePtrFunc](coreFoundation, common.CFDataGetBytePtrSym)
cfRelease := common.GetFunc[common.CFReleaseFunc](coreFoundation, common.CFReleaseSym)

matching := ioServiceMatching("AppleARMIODevice")

var iterator uint32
if status := ioServiceGetMatchingServices(common.KIOMainPortDefault, uintptr(matching), &iterator); status != common.KERN_SUCCESS {
return 0.0, fmt.Errorf("IOServiceGetMatchingServices error=%d", status)
}
defer ioObjectRelease(iterator)

pCorekey := cfStringCreateWithCString(common.KCFAllocatorDefault, "voltage-states5-sram", common.KCFStringEncodingUTF8)
defer cfRelease(uintptr(pCorekey))

var pCoreHz uint32
for {
service := ioIteratorNext(iterator)
if !(service > 0) {
break
}

buf := make([]byte, 512)
ioRegistryEntryGetName(service, &buf[0])

if common.GoString(&buf[0]) == "pmgr" {
pCoreRef := ioRegistryEntryCreateCFProperty(service, uintptr(pCorekey), common.KCFAllocatorDefault, common.KNilOptions)
length := cfDataGetLength(uintptr(pCoreRef))
data := cfDataGetBytePtr(uintptr(pCoreRef))

// composite uint32 from the byte array
buf := unsafe.Slice((*byte)(data), length)

// combine the bytes into a uint32 value
b := buf[length-8 : length-4]
pCoreHz = binary.LittleEndian.Uint32(b)
ioObjectRelease(service)
break
}

ioObjectRelease(service)
}

return float64(pCoreHz / 1_000_000), nil
}
111 changes: 0 additions & 111 deletions cpu/cpu_darwin_cgo.go

This file was deleted.

13 changes: 13 additions & 0 deletions cpu/cpu_darwin_fallback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: BSD-3-Clause
//go:build darwin && !arm64

package cpu

import "golang.org/x/sys/unix"

func getFrequency() (float64, error) {
// Use the rated frequency of the CPU. This is a static value and does not
// account for low power or Turbo Boost modes.
cpuFrequency, err := unix.SysctlUint64("hw.cpufrequency")
return float64(cpuFrequency) / 1000000.0, err
}
14 changes: 0 additions & 14 deletions cpu/cpu_darwin_nocgo.go

This file was deleted.

5 changes: 2 additions & 3 deletions cpu/cpu_darwin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ package cpu

import (
"os"
"runtime"
"testing"

"github.com/shoenig/go-m1cpu"
)

func TestInfo_AppleSilicon(t *testing.T) {
if !m1cpu.IsAppleSilicon() {
if runtime.GOARCH != "arm64" {
t.Skip("wrong cpu type")
}

Expand Down
Loading

0 comments on commit 4140cda

Please sign in to comment.