Skip to content

Commit 28cb2fe

Browse files
authored
feat: windows cpu calculation fix using PDH counters
* feat: windows cpu calculation using PDH counters * feat: bug fix with cpu darwin * feat: linting fixes and additional test cases * feat: change logic to use PdhGetRawCounterArray to handle multiple cPU groups * feat: linting fixes * feat: liniting fixes * feat: linting fixes * review fixes * feat: linting fixes * feat: linting fixes * feat: linting fixes * feat: linting fixes
1 parent a94edea commit 28cb2fe

File tree

8 files changed

+1600
-26
lines changed

8 files changed

+1600
-26
lines changed

internal/windows/api/pdh.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ type (
125125
PDH_HCOUNTER HANDLE // counter handle
126126
)
127127

128+
// Windows FILETIME structure
129+
type FILETIME struct {
130+
LowDateTime uint32
131+
HighDateTime uint32
132+
}
133+
128134
// Union specialization for double values
129135
type PDH_FMT_COUNTERVALUE_DOUBLE struct {
130136
CStatus uint32
@@ -162,6 +168,21 @@ type PDH_FMT_COUNTERVALUE_ITEM_LONG struct {
162168
FmtValue PDH_FMT_COUNTERVALUE_LONG
163169
}
164170

171+
// Raw counter value structure
172+
type PDH_RAW_COUNTER struct {
173+
CStatus uint32
174+
TimeStamp FILETIME
175+
FirstValue int64 // For rate-based counters, this is typically the numerator or the current sample value.
176+
SecondValue int64 // For rate-based counters, this is typically the denominator or the previous sample value.
177+
MultiCount uint32
178+
}
179+
180+
// Raw counter array item structure
181+
type PDH_RAW_COUNTER_ITEM struct {
182+
SzName *uint16 // pointer to a string
183+
RawValue PDH_RAW_COUNTER
184+
}
185+
165186
var (
166187
// Library
167188
libpdhDll *syscall.DLL
@@ -173,6 +194,7 @@ var (
173194
pdh_CollectQueryData *syscall.Proc
174195
pdh_GetFormattedCounterValue *syscall.Proc
175196
pdh_GetFormattedCounterArrayW *syscall.Proc
197+
pdh_GetRawCounterArrayW *syscall.Proc
176198
pdh_OpenQuery *syscall.Proc
177199
pdh_ValidatePathW *syscall.Proc
178200
)
@@ -188,6 +210,7 @@ func init() {
188210
pdh_CollectQueryData = libpdhDll.MustFindProc("PdhCollectQueryData")
189211
pdh_GetFormattedCounterValue = libpdhDll.MustFindProc("PdhGetFormattedCounterValue")
190212
pdh_GetFormattedCounterArrayW = libpdhDll.MustFindProc("PdhGetFormattedCounterArrayW")
213+
pdh_GetRawCounterArrayW = libpdhDll.MustFindProc("PdhGetRawCounterArrayW")
191214
pdh_OpenQuery = libpdhDll.MustFindProc("PdhOpenQuery")
192215
pdh_ValidatePathW = libpdhDll.MustFindProc("PdhValidatePathW")
193216

@@ -439,3 +462,38 @@ func PdhValidatePath(path string) uint32 {
439462

440463
return uint32(ret)
441464
}
465+
466+
// Returns an array of raw counter values for a counter that contains a wildcard character for the instance name.
467+
// This function is particularly useful for collecting CPU metrics from multiple CPU groups.
468+
// The itemBuffer must be a slice of type PDH_RAW_COUNTER_ITEM.
469+
func PdhGetRawCounterArray(hCounter PDH_HCOUNTER, lpdwBufferSize *uint32, lpdwBufferCount *uint32, itemBuffer *PDH_RAW_COUNTER_ITEM) uint32 {
470+
ret, _, _ := pdh_GetRawCounterArrayW.Call(
471+
uintptr(hCounter),
472+
uintptr(unsafe.Pointer(lpdwBufferSize)),
473+
uintptr(unsafe.Pointer(lpdwBufferCount)),
474+
uintptr(unsafe.Pointer(itemBuffer)))
475+
476+
return uint32(ret)
477+
}
478+
479+
// UTF16PtrToString converts a UTF16 pointer to a Go string
480+
func UTF16PtrToString(ptr *uint16) string {
481+
if ptr == nil {
482+
return ""
483+
}
484+
485+
// Find the length of the null-terminated wide string
486+
length := 0
487+
for p := ptr; *p != 0; {
488+
length++
489+
p = (*uint16)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + 2))
490+
}
491+
492+
if length == 0 {
493+
return ""
494+
}
495+
496+
// Convert to []uint16 slice and then to string
497+
slice := unsafe.Slice(ptr, length)
498+
return syscall.UTF16ToString(slice)
499+
}

internal/windows/api/pdh_test.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
//go:build windows
2+
// +build windows
3+
4+
// Copyright 2020 New Relic Corporation. All rights reserved.
5+
// SPDX-License-Identifier: Apache-2.0
6+
7+
package winapi
8+
9+
import (
10+
"syscall"
11+
"testing"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestUTF16PtrToString(t *testing.T) {
18+
t.Parallel()
19+
20+
tests := []struct {
21+
name string
22+
input string
23+
expected string
24+
}{
25+
{
26+
name: "Empty string",
27+
input: "",
28+
expected: "",
29+
},
30+
{
31+
name: "Simple ASCII string",
32+
input: "hello",
33+
expected: "hello",
34+
},
35+
{
36+
name: "String with spaces",
37+
input: "hello world",
38+
expected: "hello world",
39+
},
40+
{
41+
name: "String with numbers",
42+
input: "test123",
43+
expected: "test123",
44+
},
45+
{
46+
name: "String with special characters",
47+
input: "test@#$%",
48+
expected: "test@#$%",
49+
},
50+
{
51+
name: "Long string",
52+
input: "this is a much longer string to test the function with more data",
53+
expected: "this is a much longer string to test the function with more data",
54+
},
55+
{
56+
name: "String with unicode characters",
57+
input: "héllo wørld",
58+
expected: "héllo wørld",
59+
},
60+
{
61+
name: "Windows path-like string",
62+
input: "\\Processor(_Total)\\% Processor Time",
63+
expected: "\\Processor(_Total)\\% Processor Time",
64+
},
65+
}
66+
67+
for _, tt := range tests {
68+
t.Run(tt.name, func(t *testing.T) {
69+
t.Parallel()
70+
71+
// Convert input string to UTF16 pointer using syscall
72+
utf16Ptr, err := syscall.UTF16PtrFromString(tt.input)
73+
require.NoError(t, err, "Failed to create UTF16 pointer from string")
74+
75+
// Test the function
76+
result := UTF16PtrToString(utf16Ptr)
77+
78+
// Verify the result
79+
assert.Equal(t, tt.expected, result)
80+
})
81+
}
82+
}
83+
84+
func TestUTF16PtrToString_NilPointer(t *testing.T) {
85+
t.Parallel()
86+
87+
// Test with nil pointer
88+
result := UTF16PtrToString(nil)
89+
assert.Equal(t, "", result, "Should return empty string for nil pointer")
90+
}
91+
92+
func TestUTF16PtrToString_ZeroLengthString(t *testing.T) {
93+
t.Parallel()
94+
95+
// Create a UTF16 string that points to a null terminator (zero-length string)
96+
nullTerminator := uint16(0)
97+
ptr := &nullTerminator
98+
99+
result := UTF16PtrToString(ptr)
100+
assert.Equal(t, "", result, "Should return empty string for zero-length UTF16 string")
101+
}
102+
103+
func TestUTF16PtrToString_ManualUTF16Array(t *testing.T) {
104+
t.Parallel()
105+
106+
// Manually create a UTF16 array: "test" + null terminator
107+
utf16Array := []uint16{
108+
0x0074, // 't'
109+
0x0065, // 'e'
110+
0x0073, // 's'
111+
0x0074, // 't'
112+
0x0000, // null terminator
113+
}
114+
115+
// Get pointer to first element
116+
ptr := &utf16Array[0]
117+
118+
result := UTF16PtrToString(ptr)
119+
assert.Equal(t, "test", result, "Should correctly parse manually created UTF16 array")
120+
}
121+
122+
func TestUTF16PtrToString_EdgeCases(t *testing.T) {
123+
t.Parallel()
124+
125+
t.Run("Single character", func(t *testing.T) {
126+
t.Parallel()
127+
128+
utf16Ptr, err := syscall.UTF16PtrFromString("A")
129+
require.NoError(t, err)
130+
131+
result := UTF16PtrToString(utf16Ptr)
132+
assert.Equal(t, "A", result)
133+
})
134+
135+
t.Run("String with newlines", func(t *testing.T) {
136+
t.Parallel()
137+
138+
input := "line1\nline2\r\nline3"
139+
utf16Ptr, err := syscall.UTF16PtrFromString(input)
140+
require.NoError(t, err)
141+
142+
result := UTF16PtrToString(utf16Ptr)
143+
assert.Equal(t, input, result)
144+
})
145+
146+
t.Run("String with tabs", func(t *testing.T) {
147+
t.Parallel()
148+
149+
input := "col1\tcol2\tcol3"
150+
utf16Ptr, err := syscall.UTF16PtrFromString(input)
151+
require.NoError(t, err)
152+
153+
result := UTF16PtrToString(utf16Ptr)
154+
assert.Equal(t, input, result)
155+
})
156+
}
157+
158+
// BenchmarkUTF16PtrToString benchmarks the performance of the function.
159+
func BenchmarkUTF16PtrToString(b *testing.B) {
160+
// Create test string
161+
testString := "\\Processor(_Total)\\% Processor Time"
162+
utf16Ptr, err := syscall.UTF16PtrFromString(testString)
163+
require.NoError(b, err)
164+
165+
b.ResetTimer()
166+
167+
for range b.N {
168+
_ = UTF16PtrToString(utf16Ptr)
169+
}
170+
}
171+
172+
// BenchmarkUTF16PtrToString_LongString benchmarks with a longer string.
173+
func BenchmarkUTF16PtrToString_LongString(b *testing.B) {
174+
// Create a longer test string
175+
testString := "This is a much longer string to test the performance of the UTF16PtrToString function with more realistic data that might be encountered in real-world scenarios"
176+
utf16Ptr, err := syscall.UTF16PtrFromString(testString)
177+
require.NoError(b, err)
178+
179+
b.ResetTimer()
180+
181+
for range b.N {
182+
_ = UTF16PtrToString(utf16Ptr)
183+
}
184+
}
185+
186+
// TestUTF16PtrToString_CompareWithSyscall verifies our implementation matches syscall behavior.
187+
func TestUTF16PtrToString_CompareWithSyscall(t *testing.T) {
188+
t.Parallel()
189+
190+
testStrings := []string{
191+
"hello",
192+
"hello world",
193+
"\\Processor(_Total)\\% Processor Time",
194+
"test with unicode: héllo wørld 你好",
195+
"",
196+
"A",
197+
"special chars: !@#$%^&*()",
198+
}
199+
200+
for _, testStr := range testStrings {
201+
t.Run("Compare_"+testStr, func(t *testing.T) {
202+
t.Parallel()
203+
204+
// Convert to UTF16 slice first, then get pointer
205+
utf16Slice, err := syscall.UTF16FromString(testStr)
206+
require.NoError(t, err)
207+
208+
// Get pointer to first element (if slice is not empty)
209+
if len(utf16Slice) > 0 {
210+
ptr := &utf16Slice[0]
211+
212+
// Test our function
213+
ourResult := UTF16PtrToString(ptr)
214+
215+
// Compare with syscall's implementation
216+
syscallResult := syscall.UTF16ToString(utf16Slice[:len(utf16Slice)-1]) // Remove null terminator for syscall
217+
218+
assert.Equal(t, syscallResult, ourResult, "Our implementation should match syscall.UTF16ToString")
219+
}
220+
})
221+
}
222+
}

0 commit comments

Comments
 (0)