From c85e780e87bafa72dfe24e3ecda31888c1d99fb3 Mon Sep 17 00:00:00 2001 From: Matt Harbison Date: Tue, 17 Sep 2024 19:00:39 -0400 Subject: [PATCH 1/2] process_collector: fill in virtual and resident memory values on macOS (#1600) Unfortunately, these values aren't available from getrusage(2), or any other builtin Go API. Using cgo is one alternative. It's possible to conditionalize everything such that cgo can remain disabled on non-Darwin platforms, or even when cross-compiling Darwin executables on a non-Darwin platform (and stub in code that causes the metrics to not be exported). `CGO_ENABLED=1` is set by default on macOS, but unfortunately is off for the non-host architecture, even when gcc supports cross-compiling. (e.g. building with GOARCH=amd on an M2 mac skipped the cgo code.) I think that's too subtle of a distinction to rely on cgo. There's no builtin equivalent of `syscall.NewLazyDLL()` and `.NewProc()` on macOS that Go provides for Windows, so we're stuck with a 3rd party dependency. But it seems stable, maintained, ang getting a fair amount of usage. I'm avoiding their struct deserialization because these native structs are packed differently than the equivalent Go structs, which was causing bad values to be returned. The code is heavy with inline comments, and I tried keeping the type names the same as the C code to make it easier to search for them. I'm not sure that we need to do the `mach_vm_region()` call to adjust the `task_info()` values, because I've never seen that conditional evaluate to True on either amd64, arm64, or when amd64 is run under Rosetta. But this is what `ps(1)` does, and I think it's reasonable to try to match that unless somebody knows it's dead code. Signed-off-by: Matt Harbison --- go.mod | 1 + go.sum | 2 + prometheus/process_collector_amd64_darwin.go | 54 +++++ prometheus/process_collector_arm64_darwin.go | 55 +++++ prometheus/process_collector_darwin.go | 8 +- prometheus/process_collector_purego_darwin.go | 219 ++++++++++++++++++ 6 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 prometheus/process_collector_amd64_darwin.go create mode 100644 prometheus/process_collector_arm64_darwin.go create mode 100644 prometheus/process_collector_purego_darwin.go diff --git a/go.mod b/go.mod index 2340c7778..b5b974e0a 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( ) require ( + github.com/ebitengine/purego v0.7.1 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect diff --git a/go.sum b/go.sum index 59d98938c..d6f78b436 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA= +github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/prometheus/process_collector_amd64_darwin.go b/prometheus/process_collector_amd64_darwin.go new file mode 100644 index 000000000..8ae71d3f1 --- /dev/null +++ b/prometheus/process_collector_amd64_darwin.go @@ -0,0 +1,54 @@ +// Copyright 2024 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build amd64 + +package prometheus + +// These are macros in xnu/osfmk/mach/shared_memory_server.h +const ( + globalSharedTextSegment uint64 = 0x90000000 + + sharedTextRegionSize uint64 = 0x10000000 + sharedDataRegionSize uint64 = 0x10000000 +) + +const ( + machTaskBasicInfoSizeOf = 48 // sizeof(struct mach_task_basic_info) + vmRegionBasicInfo64SizeOf = 36 // sizeof(struct vm_region_basic_info_64) +) + +// Fundamental mach types defined in xnu/osfmk/mach/i386/vm_types.h. The integer_t and +// __darwin_natural_t types are explicitly 32-bit here to help decoding into a structure, +// where 'int' types are not supported. The __darwin_natural_t type is defined in +// xnu/bsd/i386/_types.h. +type ( + __darwin_natural_t = uint32 // typedef unsigned int __darwin_natural_t; + integer_t = int32 // typedef int integer_t; + natural_t = __darwin_natural_t // typedef __darwin_natural_t natural_t; + + mach_vm_offset_t = uint64 // typedef uint64_t mach_vm_offset_t __kernel_ptr_semantics; + mach_vm_size_t = uint64 // typedef uint64_t mach_vm_size_t; +) + +// Defined in xnu/osfmk/mach/i386/boolean.h, and explicitly 32-bit here to help decoding +// into a structure, where 'int' types are not supported. +type ( + boolean_t = uint32 // typedef unsigned int boolean_t; +) + +// Defined in xnu/osfmk/mach/i386/kern_return.h; see xnu/osfmk/mach/kern_return.h for +// possible values. +type ( + kern_return_t = int // typedef int kern_return_t; +) diff --git a/prometheus/process_collector_arm64_darwin.go b/prometheus/process_collector_arm64_darwin.go new file mode 100644 index 000000000..613e16d52 --- /dev/null +++ b/prometheus/process_collector_arm64_darwin.go @@ -0,0 +1,55 @@ +// Copyright 2024 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build arm64 + +package prometheus + +// These are macros in xnu/osfmk/mach/shared_memory_server.h. Note that __arm__ +// is not defined for 64-bit arm. +const ( + globalSharedTextSegment uint64 = 0x90000000 + + sharedTextRegionSize uint64 = 0x10000000 + sharedDataRegionSize uint64 = 0x10000000 +) + +const ( + machTaskBasicInfoSizeOf = 48 // sizeof(struct mach_task_basic_info) + vmRegionBasicInfo64SizeOf = 36 // sizeof(struct vm_region_basic_info_64) +) + +// Fundamental mach types defined in xnu/osfmk/mach/arm/vm_types.h. The integer_t and +// __darwin_natural_t types are explicitly 32-bit here to help decoding into a structure, +// where 'int' types are not supported. The __darwin_natural_t type is defined in +// xnu/bsd/arm/_types.h. +type ( + __darwin_natural_t = uint32 // typedef unsigned int __darwin_natural_t; + integer_t = int32 // typedef int integer_t; + natural_t = __darwin_natural_t // typedef __darwin_natural_t natural_t; + + mach_vm_offset_t = uint64 // typedef uint64_t mach_vm_offset_t __kernel_ptr_semantics; + mach_vm_size_t = uint64 // typedef uint64_t mach_vm_size_t; +) + +// Defined in xnu/osfmk/mach/arm/boolean.h, and explicitly 32-bit here to help decoding +// into a structure, where 'int' types are not supported. +type ( + boolean_t = int32 // typedef int boolean_t; +) + +// Defined in xnu/osfmk/mach/arm/kern_return.h; see xnu/osfmk/mach/kern_return.h for +// possible values. +type ( + kern_return_t = int // typedef int kern_return_t; +) diff --git a/prometheus/process_collector_darwin.go b/prometheus/process_collector_darwin.go index 4d9314c3d..022a8835d 100644 --- a/prometheus/process_collector_darwin.go +++ b/prometheus/process_collector_darwin.go @@ -85,7 +85,13 @@ func (c *processCollector) processCollect(ch chan<- Metric) { c.reportError(ch, c.cpuTotal, err) } - // TODO: publish c.vsize and c.rss values + if info, err := getMemoryUsage(); err == nil { + ch <- MustNewConstMetric(c.rss, GaugeValue, float64(info.ResidentSize)) + ch <- MustNewConstMetric(c.vsize, GaugeValue, float64(info.VirtualSize)) + } else { + c.reportError(ch, c.rss, err) + c.reportError(ch, c.vsize, err) + } if fds, err := getOpenFileCount(); err == nil { ch <- MustNewConstMetric(c.openFDs, GaugeValue, fds) diff --git a/prometheus/process_collector_purego_darwin.go b/prometheus/process_collector_purego_darwin.go new file mode 100644 index 000000000..fd35a437c --- /dev/null +++ b/prometheus/process_collector_purego_darwin.go @@ -0,0 +1,219 @@ +// Copyright 2024 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus + +import ( + "bytes" + "encoding/binary" + "fmt" + "github.com/ebitengine/purego" +) + +func init() { + if lib, err := purego.Dlopen("/usr/lib/system/libsystem_kernel.dylib", + purego.RTLD_NOW|purego.RTLD_GLOBAL); err == nil { + + // purego.RegisterLibFunc() panics if the symbol is missing. Ignore any error, + // and the metric will simply be unavailable when it is queried, instead of + // bringing down the whole process. + defer func() { + if err := recover(); err != nil { + // TODO: Log this somehow + } + }() + + purego.RegisterLibFunc(&machTaskSelf, lib, "mach_task_self") + purego.RegisterLibFunc(&taskInfo, lib, "task_info") + purego.RegisterLibFunc(&machVmRegion, lib, "mach_vm_region") + } +} + +const ( + // The task_info() flavor MACH_TASK_BASIC_INFO for retrieving machTaskBasicInfo. + mach_task_basic_info task_flavor_t = 20 /* always 64-bit basic info */ + + // The MACH_TASK_BASIC_INFO_COUNT value, which is passed to the Mach API as the size + // of the payload for MACH_TASK_BASIC_INFO commands. + machTaskBasicInfoCount mach_msg_type_number_t = machTaskBasicInfoSizeOf / 4 +) + +// Defined in xnu/osfmk/mach/policy.h, xnu/iokit/IOKit/IORPC.h, and xnu/osfmk/mach/message.h +// respectively. policy_t is explicitly 32-bit here to help decoding into a structure, +// where 'int' types are not supported. +type ( + policy_t = int32 // typedef int policy_t; + mach_port_t = natural_t // typedef natural_t mach_port_t; + mach_msg_type_number_t = natural_t // typedef natural_t mach_msg_type_number_t; +) + +// Defined in xnu/osfmk/mach/task_info.h. Note that task_info_t is actually defined as +// integer_t* and cast from the address of the structure in C. Define it as []byte to +// keep the type system happy. +type ( + task_flavor_t = natural_t // typedef natural_t task_flavor_t + task_info_t = []byte // typedef integer_t *task_info_t /* varying array of int */ +) + +// time_value_t is the type for kernel time values, defined in xnu/osfmk/mach/time_value.h +type time_value_t struct { + Seconds integer_t + MicroSeconds integer_t +} + +var machTaskSelf func() mach_port_t + +var taskInfo func( + mach_port_t, + task_flavor_t, + task_info_t, + *mach_msg_type_number_t, +) kern_return_t + +// machTaskBasicInfo is the representation of `struct mach_task_basic_info` defined in +// xnu/osfmk/mach/task_info.h, which is the architecture independent payload for fetching +// certain task values. +type machTaskBasicInfo struct { + VirtualSize mach_vm_size_t // virtual memory size (bytes) + ResidentSize mach_vm_size_t // resident memory size (bytes) + ResidentSizeMax mach_vm_size_t // maximum resident memory size (bytes) + UserTime time_value_t // total user run time for terminated threads + SystemTime time_value_t // total system run time for terminated threads + Policy policy_t // default policy for new threads + SuspendCount integer_t // suspend count for task +} + +func getBasicTaskInfo() (*machTaskBasicInfo, error) { + var info machTaskBasicInfo + + if taskInfo == nil { + return nil, fmt.Errorf("task_info() is not supported") + } + + var count = machTaskBasicInfoCount + buf := make([]byte, machTaskBasicInfoSizeOf) + + if ret := taskInfo(machTaskSelf(), mach_task_basic_info, buf, &count); ret != 0 { + return nil, fmt.Errorf("task_info() returned %d", ret) + } + + if err := loadStruct(buf, &info); err != nil { + return nil, err + } + + return &info, nil +} + +// Defined in xnu/osfmk/mach/memory_object_types.h, xnu/osfmk/mach/vm_behavior.h, +// defined in xnu/osfmk/mach/vm_inherit.h, and defined in xnu/osfmk/mach/vm_prot.h +// respectively. These types are not identical to the native definitions, because the +// struct decoding requires primitives with a specific width. The widths here are the +// same as the native types. +type ( + memory_object_offset_t = uint64 // typedef unsigned long long memory_object_offset_t; + vm_behavior_t = int32 // typedef int vm_behavior_t + vm_inherit_t = uint32 // typedef unsigned int vm_inherit_t; + vm_prot_t = int32 // typedef int vm_prot_t; +) + +// These are defined in xnu/osfmk/mach/vm_types.h. +type ( + vm_map_t = mach_port_t // typedef mach_port_t vm_map_t; +) + +// Defined in xnu/osfmk/mach/vm_region.h. vm_region_flavor_t is explicitly 32-bit here to +// help decoding into a structure, where 'int' types are not supported, and vm_region_info_t +// is []byte to keep the type system happy. +type ( + vm_region_flavor_t = int32 // typedef int vm_region_flavor_t; + vm_region_info_t = []byte // typedef int *vm_region_info_t; +) + +const ( + // The mach_vm_region() flavor VM_REGION_BASIC_INFO_64 for retrieving + // vmRegionBasicInfo64. + vm_region_basic_info_64 vm_region_flavor_t = 9 + + // The VM_REGION_BASIC_INFO_COUNT_64 value, which is passed to the Mach API as the + // size of the payload for VM_REGION_BASIC_INFO_64 commands. + vmRegionBasicInfoCount64 mach_msg_type_number_t = vmRegionBasicInfo64SizeOf / 4 +) + +// vmRegionBasicInfo64 is the representation of `struct vm_region_basic_info_64` defined +// in xnu/osfmk/mach/vm_region.h. This is enclosed in `#pragma pack(push, 4)` in C, so +// unsafe.SizeOf() won't match the actual sizeof(vm_region_basic_info_64). +type vmRegionBasicInfo64 struct { + Protection vm_prot_t + MaxProtection vm_prot_t + Inheritance vm_inherit_t + Shared boolean_t + Reserved boolean_t + Offset memory_object_offset_t + Behavior vm_behavior_t + UserWiredCount uint16 +} + +var machVmRegion func( + vm_map_t, + *mach_vm_offset_t, /* IN/OUT */ + *mach_vm_size_t, /* OUT */ + vm_region_flavor_t, /* IN */ + vm_region_info_t, /* OUT */ + *mach_msg_type_number_t, /* IN/OUT */ + *mach_port_t, /* OUT */ +) kern_return_t + +func getMemoryUsage() (*machTaskBasicInfo, error) { + // The logic in here follows how the ps(1) utility determines the memory values. The + // basic_task_info command used here is a more modern, cross-architecture one that is + // suggested in the kernel header files. + // + // https://github.com/apple-oss-distributions/adv_cmds/blob/8744084ea0ff41ca4bb96b0f9c22407d0e48e9b7/ps/tasks.c#L132 + + info, err := getBasicTaskInfo() + + if err != nil { + return nil, err + } else if machVmRegion != nil { + + var textInfo vmRegionBasicInfo64 + + buf := make([]byte, vmRegionBasicInfo64SizeOf) + address := globalSharedTextSegment + var size mach_vm_size_t + var objectName mach_port_t + + cmd := vm_region_basic_info_64 + count := vmRegionBasicInfoCount64 + + ret := machVmRegion(machTaskSelf(), &address, &size, cmd, buf, &count, &objectName) + + if ret == 0 { + if err := loadStruct(buf, &textInfo); err == nil { + adjustment := sharedTextRegionSize + sharedDataRegionSize + if textInfo.Reserved != 0 && size == sharedTextRegionSize && info.VirtualSize > adjustment { + info.VirtualSize -= adjustment + } + } + } + } + + return info, nil +} + +func loadStruct(buffer []byte, data any) error { + r := bytes.NewReader(buffer) + + // TODO: NativeEndian was added in go 1.21 + return binary.Read(r, binary.LittleEndian, data) +} From 4817b8f6697406c12d0e429fdab1d22f1e4eab16 Mon Sep 17 00:00:00 2001 From: Matt Harbison Date: Tue, 17 Sep 2024 19:08:36 -0400 Subject: [PATCH 2/2] tests: add coverage for the virtual and resident memory support on macOS Here we have to use cgo, but since it's conditionalized to 1) tests, 2) only on Darwin, 3) when `CGO_ENABLED=1`, and 4) gracefully skips when it is disabled, I think it's OK. There were many layers to the C types that are defined, and subtle sizing and offset issues that the C compiler would handle that need to be correct here, so I think it's worth having around. I don't expect any of the sizes or offsets to change until a new architecture is supported for macOS, but these are definitely not the more documented APIs. Signed-off-by: Matt Harbison --- .../process_collector_purego_darwin_test.go | 214 ++++++++++++++++++ .../purego/process_collector_cgo_darwin.c | 80 +++++++ .../purego/process_collector_cgo_darwin.go | 77 +++++++ 3 files changed, 371 insertions(+) create mode 100644 prometheus/process_collector_purego_darwin_test.go create mode 100644 prometheus/testutil/purego/process_collector_cgo_darwin.c create mode 100644 prometheus/testutil/purego/process_collector_cgo_darwin.go diff --git a/prometheus/process_collector_purego_darwin_test.go b/prometheus/process_collector_purego_darwin_test.go new file mode 100644 index 000000000..36fa49e2d --- /dev/null +++ b/prometheus/process_collector_purego_darwin_test.go @@ -0,0 +1,214 @@ +// Copyright 2024 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build cgo && darwin + +package prometheus + +import ( + "bytes" + "encoding/binary" + "github.com/prometheus/client_golang/prometheus/testutil/purego" + "math" + "reflect" + "testing" +) + +// TestNativeTypeMatches ensures that const values for preprocessor macros, native struct +// sizes, and field offsets are in sync with the native code. +func TestNativeTypeMatches(t *testing.T) { + tests := []struct { + name string + want uint64 + got func() uint64 + }{ + { + "sizeof(machTaskBasicInfo)", + machTaskBasicInfoSizeOf, + purego.GetSizeofMachTaskBasicInfo, + }, + { + "sizeof(machTaskBasicInfo.VirtualSize)", + 8, + purego.GetSizeofMachTaskBasicInfo_virtual_size, + }, + { + "sizeof(vmRegionBasicInfo64.ResidentSize)", + 8, + purego.GetSizeofMachTaskBasicInfo_resident_size, + }, + { + "sizeof(vmRegionBasicInfo64)", + vmRegionBasicInfo64SizeOf, + purego.GetSizeofVmRegionBasicInfo64, + }, + { + "sizeof(vmRegionBasicInfo64.Reserved)", + 4, + purego.GetSizeofVmRegionBasicInfo64_reserved, + }, + { + "value of globalSharedTextSegment", + globalSharedTextSegment, + purego.GetGlobalSharedTextSegment, + }, + { + "value of sharedTextRegionSize", + sharedTextRegionSize, + purego.GetSharedTextRegionSize, + }, + { + "value of sharedDataRegionSize", + sharedDataRegionSize, + purego.GetSharedDataRegionSize, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := test.got() + if test.want != got { + t.Errorf("Expected %d, got %d\n", test.want, got) + } + }) + } +} + +// TestNativeStructMapping ensures that fields of a proper size and at the proper offset +// in a byte array can properly initialize a corresponding structure. Since the field +// offset in a Go struct may not match the offset in the C struct, the most robust test +// is to ensure that a field value written to a specific byte buffer offset is mapped to +// the corresponding Go struct field. The value should be the maximum, to ensure the +// entire byte range is mapped to the structure field. +func TestNativeStructMapping(t *testing.T) { + tests := []struct { + name string // The name of the test + field string // The structure field to test + value reflect.Value // The value of the field to set and validate + offset func() uint64 // The offset of the field in the native struct + fieldSize func() uint64 // The native width of the field in the struct + structSize func() uint64 // The native size of the C structure + dataType reflect.Type + }{ + { + "machTaskBasicInfo_VirtualSize", + "VirtualSize", + reflect.ValueOf(mach_vm_size_t(math.MaxUint64)), + purego.GetOffsetOfMachTaskBasicInfo_virtual_size, + purego.GetSizeofMachTaskBasicInfo_virtual_size, + purego.GetSizeofMachTaskBasicInfo, + reflect.TypeOf(machTaskBasicInfo{}), + }, + { + "machTaskBasicInfo_ResidentSize", + "ResidentSize", + reflect.ValueOf(mach_vm_size_t(math.MaxUint64)), + purego.GetOffsetOfMachTaskBasicInfo_resident_size, + purego.GetSizeofMachTaskBasicInfo_resident_size, + purego.GetSizeofMachTaskBasicInfo, + reflect.TypeOf(machTaskBasicInfo{}), + }, + { + "vmRegionBasicInfo64_Reserved", + "Reserved", + reflect.ValueOf(boolean_t(math.MaxInt32)), + purego.GetOffsetOfVmRegionBasicInfo64_reserved, + purego.GetSizeofVmRegionBasicInfo64_reserved, + purego.GetSizeofVmRegionBasicInfo64, + reflect.TypeOf(vmRegionBasicInfo64{}), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + offset := test.offset() + fieldSize := test.fieldSize() + structSize := test.structSize() + + buf := bytes.NewBuffer(make([]byte, 0, structSize)) + order := binary.LittleEndian + + // Move the position to the offset of the field + for i := uint64(0); i < offset; i++ { + var pad byte = 0 + if err := binary.Write(buf, order, pad); err != nil { + t.Error(err) + return + } + } + + // Write the test value at the desired offset. + if err := binary.Write(buf, order, test.value.Interface()); err != nil { + t.Error(err) + return + } + + // Zero fill the rest of the buffer to avoid a premature EOF. + for i := offset + fieldSize; i < structSize; i++ { + var pad byte = 0 + if err := binary.Write(buf, order, pad); err != nil { + t.Error(err) + return + } + } + + // Instantiate a new structure, decode the buffer into it, and then compare + // the structure field to the expected value. + ptr := reflect.New(test.dataType) + + if err := binary.Read(buf, order, ptr.Interface()); err != nil { + t.Error(err) + return + } + + field := ptr.Elem().FieldByName(test.field) + + if field.IsZero() { + t.Errorf("Missing field %s\n", test.field) + return + } + + if field.CanInt() { + got := field.Int() + + if got != test.value.Int() { + t.Errorf("Got %d, wanted %s\n", got, test.value) + return + } + } else if field.CanUint() { + got := field.Uint() + + if got != test.value.Uint() { + t.Errorf("Got %d, wanted %s\n", got, test.value) + return + } + } else { + t.Errorf("Unhandled field type: %s\n", field.Type()) + } + }) + } +} + +func TestSyscall(t *testing.T) { + // There's not a good way to validate that the value returned from the syscall is + // accurate, but we can ensure that the function pointer is non-nil, and that no + // error is returned. + + if taskInfo == nil { + t.Errorf("No task_info() method found\n") + } + + if _, err := getMemoryUsage(); err != nil { + t.Errorf("getMemoryUsage() failed with %v\n", err) + } +} diff --git a/prometheus/testutil/purego/process_collector_cgo_darwin.c b/prometheus/testutil/purego/process_collector_cgo_darwin.c new file mode 100644 index 000000000..312c1afa8 --- /dev/null +++ b/prometheus/testutil/purego/process_collector_cgo_darwin.c @@ -0,0 +1,80 @@ +// Copyright 2024 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build cgo && darwin + +#include +#include +// Compiler warns shared_memory_server.h is deprecated, use this instead. +// But this doesn't define SHARED_DATA_REGION_SIZE and SHARED_TEXT_REGION_SIZE. +//#include +#include +#include +#include + + +size_t getGlobalSharedTextSegment() +{ + return GLOBAL_SHARED_TEXT_SEGMENT; +} +size_t getSharedTextRegionSize() +{ + return SHARED_TEXT_REGION_SIZE; +} +size_t getSharedDataRegionSize() +{ + return SHARED_DATA_REGION_SIZE; +} + +size_t getSizeofMachTaskBasicInfo() +{ + return sizeof(struct mach_task_basic_info); +} + +size_t getOffsetOfMachTaskBasicInfo_virtual_size() +{ + return offsetof(struct mach_task_basic_info, virtual_size); +} + +size_t getSizeofMachTaskBasicInfo_virtual_size() +{ + struct mach_task_basic_info info; + return sizeof(info.virtual_size); +} + +size_t getOffsetOfMachTaskBasicInfo_resident_size() +{ + return offsetof(struct mach_task_basic_info, resident_size); +} + +size_t getSizeofMachTaskBasicInfo_resident_size() +{ + struct mach_task_basic_info info; + return sizeof(info.resident_size); +} + +size_t getSizeofVmRegionBasicInfo64() +{ + return sizeof(struct vm_region_basic_info_64); +} + +size_t getOffsetOfVmRegionBasicInfo64_reserved() +{ + return offsetof(struct vm_region_basic_info_64, reserved); +} + +size_t getSizeofVmRegionBasicInfo64_reserved() +{ + struct vm_region_basic_info_64 data; + return sizeof(data.reserved); +} diff --git a/prometheus/testutil/purego/process_collector_cgo_darwin.go b/prometheus/testutil/purego/process_collector_cgo_darwin.go new file mode 100644 index 000000000..cf200f251 --- /dev/null +++ b/prometheus/testutil/purego/process_collector_cgo_darwin.go @@ -0,0 +1,77 @@ +// Copyright 2024 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build cgo && darwin + +package purego + +/* +size_t getGlobalSharedTextSegment(); +size_t getSharedTextRegionSize(); +size_t getSharedDataRegionSize(); + +size_t getSizeofMachTaskBasicInfo(); +size_t getOffsetOfMachTaskBasicInfo_virtual_size(); +size_t getSizeofMachTaskBasicInfo_virtual_size(); +size_t getOffsetOfMachTaskBasicInfo_resident_size(); +size_t getSizeofMachTaskBasicInfo_resident_size(); + +size_t getSizeofVmRegionBasicInfo64(); +size_t getOffsetOfVmRegionBasicInfo64_reserved(); +size_t getSizeofVmRegionBasicInfo64_reserved(); +*/ +import "C" + +func GetGlobalSharedTextSegment() uint64 { + return uint64(C.getGlobalSharedTextSegment()) +} + +func GetSharedTextRegionSize() uint64 { + return uint64(C.getSharedTextRegionSize()) +} + +func GetSharedDataRegionSize() uint64 { + return uint64(C.getSharedDataRegionSize()) +} + +func GetSizeofMachTaskBasicInfo() uint64 { + return uint64(C.getSizeofMachTaskBasicInfo()) +} + +func GetOffsetOfMachTaskBasicInfo_virtual_size() uint64 { + return uint64(C.getOffsetOfMachTaskBasicInfo_virtual_size()) +} + +func GetSizeofMachTaskBasicInfo_virtual_size() uint64 { + return uint64(C.getSizeofMachTaskBasicInfo_virtual_size()) +} + +func GetOffsetOfMachTaskBasicInfo_resident_size() uint64 { + return uint64(C.getOffsetOfMachTaskBasicInfo_resident_size()) +} + +func GetSizeofMachTaskBasicInfo_resident_size() uint64 { + return uint64(C.getSizeofMachTaskBasicInfo_resident_size()) +} + +func GetSizeofVmRegionBasicInfo64() uint64 { + return uint64(C.getSizeofVmRegionBasicInfo64()) +} + +func GetOffsetOfVmRegionBasicInfo64_reserved() uint64 { + return uint64(C.getOffsetOfVmRegionBasicInfo64_reserved()) +} + +func GetSizeofVmRegionBasicInfo64_reserved() uint64 { + return uint64(C.getSizeofVmRegionBasicInfo64_reserved()) +}