performancecounter: fix explicit instance lists and startup panics on invalid config (#2511)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2026-10-07 14:34:26 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 0e45202032
commit 7b20daa9ac
8 changed files with 254 additions and 49 deletions
+31 -20
View File
@@ -334,7 +334,7 @@ func (c *Collector) collectWorkerRaw() {
elemValue := reflect.ValueOf(reflect.New(elemType).Interface()).Elem()
indexMap := map[string]int{}
stringMap := map[*uint16]string{}
nameCache := map[string]string{}
for _, counter := range c.counters {
for _, instance := range counter.Instances {
@@ -365,11 +365,6 @@ func (c *Collector) collectWorkerRaw() {
items = unsafe.Slice((*RawCounterItem)(unsafe.Pointer(&buf[0])), itemCount)
var (
instanceName string
ok bool
)
for _, item := range items {
if item.RawValue.CStatus != CstatusValidData && item.RawValue.CStatus != CstatusNewData {
c.logger.Debug("skipping counter item with invalid data status",
@@ -381,10 +376,7 @@ func (c *Collector) collectWorkerRaw() {
continue
}
if instanceName, ok = stringMap[item.SzName]; !ok {
instanceName = windows.UTF16PtrToString(item.SzName)
stringMap[item.SzName] = instanceName
}
instanceName := decodeInstanceName(nameCache, item.SzName)
if strings.HasSuffix(instanceName, InstanceTotal) && !c.totalCounterRequested {
continue
@@ -501,7 +493,7 @@ func (c *Collector) collectWorkerFormatted() {
elemValue := reflect.ValueOf(reflect.New(elemType).Interface()).Elem()
indexMap := map[string]int{}
stringMap := map[*uint16]string{}
nameCache := map[string]string{}
for _, counter := range c.counters {
for _, instance := range counter.Instances {
@@ -532,20 +524,12 @@ func (c *Collector) collectWorkerFormatted() {
items = unsafe.Slice((*FmtCounterValueItemDouble)(unsafe.Pointer(&buf[0])), itemCount)
var (
instanceName string
ok bool
)
for _, item := range items {
if item.FmtValue.CStatus != CstatusValidData && item.FmtValue.CStatus != CstatusNewData {
continue
}
if instanceName, ok = stringMap[item.SzName]; !ok {
instanceName = windows.UTF16PtrToString(item.SzName)
stringMap[item.SzName] = instanceName
}
instanceName := decodeInstanceName(nameCache, item.SzName)
if strings.HasSuffix(instanceName, InstanceTotal) && !c.totalCounterRequested {
continue
@@ -631,6 +615,33 @@ func formatCounterPath(object, instance, counterName string) string {
return counterPath
}
// decodeInstanceName returns the instance name p points to.
// Decoded names are cached by their raw UTF-16 content and not by p, because p points
// into the item buffer, which is overwritten by every Get*CounterArray call. With explicit
// instances, every call writes its single name at the same address.
func decodeInstanceName(cache map[string]string, p *uint16) string {
if p == nil {
return ""
}
n := 0
for *(*uint16)(unsafe.Add(unsafe.Pointer(p), n*2)) != 0 {
n++
}
raw := unsafe.Slice((*byte)(unsafe.Pointer(p)), n*2)
// The compiler does not allocate for the string conversion in a map lookup.
if name, ok := cache[string(raw)]; ok {
return name
}
name := windows.UTF16ToString(unsafe.Slice(p, n))
cache[string(raw)] = name
return name
}
func isKnownCounterDataError(err error) bool {
var pdhErr *Error
+64
View File
@@ -71,3 +71,67 @@ func TestCollector(t *testing.T) {
})
}
}
type processorInformation struct {
Name string
ProcessorTime float64 `perfdata:"% Processor Time"`
IdleTime float64 `perfdata:"% Idle Time"`
}
// TestCollectorExplicitInstances verifies that a collector built with an explicit
// list of instances returns one row per requested instance. Each instance has its
// own counter handle, and PDH writes every single-item result into the same buffer,
// so the instance names must not be resolved through a cache keyed by buffer address.
func TestCollectorExplicitInstances(t *testing.T) {
t.Parallel()
logger := slog.New(slog.DiscardHandler)
discovery, err := pdh.NewCollector[processorInformation](logger, pdh.CounterTypeRaw, "Processor Information", pdh.InstancesAll)
require.NoError(t, err)
t.Cleanup(discovery.Close)
var all []processorInformation
require.NoError(t, discovery.Collect(&all))
instances := make([]string, 0, 3)
for _, row := range all {
if len(instances) == cap(instances) {
break
}
instances = append(instances, row.Name)
}
if len(instances) < 2 {
t.Skipf("need at least 2 Processor Information instances, got %v", instances)
}
for _, counterType := range []pdh.CounterType{pdh.CounterTypeRaw, pdh.CounterTypeFormatted} {
t.Run(string(counterType), func(t *testing.T) {
t.Parallel()
collector, err := pdh.NewCollector[processorInformation](logger, counterType, "Processor Information", instances)
require.NoError(t, err)
t.Cleanup(collector.Close)
// Formatted rate counters need two samples before they report a value.
time.Sleep(100 * time.Millisecond)
var data []processorInformation
require.NoError(t, collector.Collect(&data))
names := make([]string, 0, len(data))
for _, row := range data {
names = append(names, row.Name)
}
require.ElementsMatch(t, instances, names)
})
}
}