mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 18:19:04 +02:00
348 lines
9.9 KiB
Go
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)
|
|
}
|
|
}
|