Files
windows_exporter/internal/pdh/collector_rows_internal_test.go
T

348 lines
9.9 KiB
Go

// SPDX-License-Identifier: Apache-2.0
//
// Copyright 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 windows
package pdh
import (
"log/slog"
"reflect"
"runtime"
"testing"
"unsafe"
"github.com/stretchr/testify/require"
"golang.org/x/sys/windows"
)
type embeddedCounters struct {
Name string
Inner float64 `perfdata:"Inner"`
}
type pointerCounters struct {
Pointer float64 `perfdata:"Pointer"`
}
type structFieldsValues struct {
_ int32 // Gives the embedded struct a non-zero offset.
embeddedCounters //nolint:embeddedstructfieldcheck // Tests the offset of a promoted field.
*pointerCounters
Outer float64 `perfdata:"Outer"`
Base float64 `perfdata:"Outer,secondvalue"`
Invalid int64 `perfdata:"Invalid"`
}
// TestStructFields checks that the counter fields of a struct, including
// fields promoted from an embedded struct, are written at their offsets.
func TestStructFields(t *testing.T) {
t.Parallel()
fields, rows, errs, err := structFields[structFieldsValues]()
require.NoError(t, err)
require.Len(t, errs, 2)
require.ErrorContains(t, errs[0], "field Pointer must not be promoted through an embedded pointer")
require.ErrorContains(t, errs[1], "field Invalid must be a float64")
offsets := make(map[string]int, len(fields))
for _, f := range fields {
offsets[f.tag] = f.index
}
require.Len(t, offsets, 3)
var row structFieldsValues
row.Invalid = 42
rows.resetRow(&row, "instance", 0)
require.Equal(t, structFieldsValues{Name: "instance"}, row)
rows.setValue(&row, offsets["Inner"], 1)
rows.setValue(&row, offsets["Outer"], 2)
rows.setValue(&row, offsets["Outer,secondvalue"], 3)
require.Equal(t, structFieldsValues{
Name: "instance",
Inner: 1,
Outer: 2,
Base: 3,
}, row)
_, _, _, err = structFields[float64]()
require.ErrorContains(t, err, "expected a struct, got float64")
}
// TestStructFieldsIndexIsByteOffset checks that the index of a counter field
// is the byte offset that structRowAccessor writes to, not a field number.
func TestStructFieldsIndexIsByteOffset(t *testing.T) {
t.Parallel()
type values struct {
Name string
A float64 `perfdata:"A"`
B float64 `perfdata:"B"`
C float64 `perfdata:"B,secondvalue"`
}
fields, _, errs, err := structFields[values]()
require.NoError(t, err)
require.Empty(t, errs)
var row values
require.Equal(t, []int{
int(unsafe.Offsetof(row.A)),
int(unsafe.Offsetof(row.B)),
int(unsafe.Offsetof(row.C)),
}, []int{fields[0].index, fields[1].index, fields[2].index})
}
// TestValidateFloatOffsets checks that field numbers or other indexes that do
// not address a float64 field are rejected before they are written through.
func TestValidateFloatOffsets(t *testing.T) {
t.Parallel()
type values struct {
Name string
A float64 `perfdata:"A"`
B float64 `perfdata:"B"`
C float64 `perfdata:"C"`
}
valueType := reflect.TypeFor[values]()
var row values
valid := []counterField{
{tag: "A", index: int(unsafe.Offsetof(row.A))},
{tag: "C", index: int(unsafe.Offsetof(row.C))},
}
require.NoError(t, validateFloatOffsets(valueType, valid))
// Field numbers, as reflect.StructField.Index[0] would give them.
fieldNumbers := make([]counterField, 0, 3)
for _, f := range reflect.VisibleFields(valueType) {
if tag, ok := f.Tag.Lookup("perfdata"); ok {
fieldNumbers = append(fieldNumbers, counterField{tag: tag, index: f.Index[0]})
}
}
require.ErrorContains(t, validateFloatOffsets(valueType, fieldNumbers), "is not the byte offset of a float64 field")
for _, index := range []int{
int(unsafe.Offsetof(row.Name)), // a string, not a float64
int(unsafe.Offsetof(row.A)) + 4, // misaligned
int(unsafe.Sizeof(row)), // past the end
int(unsafe.Sizeof(row)) - 4, // overlaps the end
-8, // before the start
} {
require.Error(t, validateFloatOffsets(valueType, []counterField{{tag: "X", index: index}}), "index %d", index)
}
}
// TestStructRowAccessorWithoutName checks that a struct without a Name field
// only gets its values.
func TestStructRowAccessorWithoutName(t *testing.T) {
t.Parallel()
type values struct {
Value float64 `perfdata:"Value"`
}
fields, rows, errs, err := structFields[values]()
require.NoError(t, err)
require.Empty(t, errs)
require.Len(t, fields, 1)
row := values{Value: 1}
rows.resetRow(&row, "instance", 1)
require.Equal(t, values{}, row)
require.False(t, rows.keepsOccurrences)
rows.setValue(&row, fields[0].index, 2)
require.Equal(t, values{Value: 2}, row)
}
// TestDynamicRowAccessor checks that a reused Row keeps a large enough Values
// array and that no value of an earlier sample remains.
func TestDynamicRowAccessor(t *testing.T) {
t.Parallel()
rows := dynamicRowAccessor(2)
values := []float64{1, 2, 3}
row := Row{Name: "old", Occurrence: 3, Values: values}
rows.resetRow(&row, "new", 1)
require.Equal(t, Row{Name: "new", Occurrence: 1, Values: []float64{0, 0}}, row)
require.Same(t, unsafe.SliceData(values), unsafe.SliceData(row.Values), "Values allocated again")
rows.setValue(&row, 1, 4)
require.Equal(t, []float64{0, 4}, row.Values)
row = Row{Name: "old", Occurrence: 2, Values: []float64{1}}
rows.resetRow(&row, "new", 0)
require.Equal(t, Row{Name: "new", Values: []float64{0, 0}}, row)
}
// TestStructRowAccessorOccurrence checks that a reused struct row gets the
// occurrence of its new instance, not that of the previous sample.
func TestStructRowAccessorOccurrence(t *testing.T) {
t.Parallel()
type values struct {
Name string
Value float64 `perfdata:"Value"`
Occurrence int
}
_, rows, errs, err := structFields[values]()
require.NoError(t, err)
require.Empty(t, errs)
require.True(t, rows.keepsOccurrences)
row := values{Name: "old", Value: 1, Occurrence: 3}
rows.resetRow(&row, "new", 1)
require.Equal(t, values{Name: "new", Occurrence: 1}, row)
rows.resetRow(&row, "first", 0)
require.Equal(t, values{Name: "first"}, row)
}
// TestRowSetReusesRows checks that a sample reuses the backing array of dst
// and that no value of an earlier sample remains in a reused row.
func TestRowSetReusesRows(t *testing.T) {
t.Parallel()
c := newRowSetTestCollector(false)
c.counters[1].FieldIndexSecondValue = -1
dst := make([]rowSetValues, 3, 4)
for i := range dst {
dst[i] = rowSetValues{Name: "stale", A: 9, B: 9, BSecond: 9, Optional: 9}
}
backing := unsafe.SliceData(dst)
dst = dst[:0]
rows := newRowSet(c, &dst, &collectState{})
addDuplicateTestItems(&rows, CounterTypeRaw, 0, []string{"b", "c"}, []float64{4, 5})
addDuplicateTestItems(&rows, CounterTypeRaw, 1, []string{"b", "c"}, []float64{6, 7})
rows.finish()
require.Same(t, backing, unsafe.SliceData(dst), "rows allocated again")
require.Equal(t, []rowSetValues{
{Name: "b", A: 4, B: 6},
{Name: "c", A: 5, B: 7},
}, dst)
}
// TestRowSetSampleDoesNotAllocate checks that a sample of known instances,
// including duplicate names, into a reused slice does not allocate.
func TestRowSetSampleDoesNotAllocate(t *testing.T) {
c := newRowSetTestCollector(true)
const instances = 100
items := make([]RawCounterItem, instances)
for i := range items {
items[i] = RawCounterItem{
// Every name occurs four times, like svchost.
SzName: windows.StringToUTF16Ptr("instance_" + string(rune('A'+i%25))),
RawValue: RawCounter{CStatus: CstatusValidData, FirstValue: int64(i)},
}
}
buf := unsafe.Slice((*byte)(unsafe.Pointer(unsafe.SliceData(items))), len(items)*int(unsafe.Sizeof(RawCounterItem{})))
state := &collectState{}
var dst []rowSetValues
sample := func() {
dst = dst[:0]
rows := newRowSet(c, &dst, state)
rows.addRawItems(0, buf, instances)
rows.addRawItems(1, buf, instances)
rows.finish()
state.names.removeUnseen()
state.keys = rows.keys
state.valid = rows.valid
}
sample()
require.Len(t, dst, instances)
require.Equal(t, "instance_A", dst[75].Name)
require.Equal(t, 3, dst[75].Occurrence)
require.Zero(t, testing.AllocsPerRun(10, sample))
runtime.KeepAlive(items)
}
// TestCollectReusesRows checks that Collect keeps the backing array of dst and
// releases the rows a sample does not use.
func TestCollectReusesRows(t *testing.T) {
t.Parallel()
type processor struct {
Name string
ProcessorTime float64 `perfdata:"% Processor Time"`
}
c, err := NewCollector[processor](slog.New(slog.DiscardHandler), CounterTypeRaw, "Processor", InstancesAll)
require.NoError(t, err)
t.Cleanup(c.Close)
dst := make([]processor, 1024)
for i := range dst {
dst[i] = processor{Name: "stale", ProcessorTime: -1}
}
backing := unsafe.SliceData(dst)
require.NoError(t, c.Collect(&dst))
require.NotEmpty(t, dst)
require.Less(t, len(dst), 1024)
require.Same(t, backing, unsafe.SliceData(dst), "rows allocated again")
for _, row := range dst {
require.NotEqual(t, "stale", row.Name)
require.GreaterOrEqual(t, row.ProcessorTime, 0.0)
}
for _, row := range dst[len(dst):cap(dst)] {
require.Equal(t, processor{}, row, "row of an earlier sample kept")
}
}
func TestIsKnownCounterDataStatus(t *testing.T) {
t.Parallel()
for _, status := range []uint32{InvalidData, CalcNegativeDenominator, CalcNegativeValue, CstatusInvalidData, CstatusNoInstance, NoData} {
require.True(t, isKnownCounterDataStatus(status), "0x%08X", status)
}
for _, status := range []uint32{ErrorSuccess, MoreData, CstatusNoObject, CstatusNoCounter, InvalidHandle} {
require.False(t, isKnownCounterDataStatus(status), "0x%08X", status)
}
}