pdh: convert all raw time counter types to seconds (#2776)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jan-Otto Kröpke
2026-10-16 22:54:44 +02:00
committed by GitHub
co-authored by Claude
parent a9d5bbef48
commit da7bebc6b3
23 changed files with 313 additions and 246 deletions
+31 -12
View File
@@ -31,6 +31,7 @@ import (
"sync"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/kernel32"
"github.com/prometheus-community/windows_exporter/internal/osversion"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/sys/windows"
@@ -487,10 +488,10 @@ func newCollector[T any](logger *slog.Logger, query *Query, resultType CounterTy
continue
}
case PERF_AVERAGE_TIMER:
// Only [Collector.Frequency] uses the time base of an average
// timer, so a missing one is not an error. Collect retries it.
if ret := GetCounterTimeBase(counterHandle, &counter.Frequency); ret != ErrorSuccess {
counter.Frequency = 0
// The time base of an average timer is the performance counter
// frequency, so use that if PDH has none.
if ret := GetCounterTimeBase(counterHandle, &counter.Frequency); ret != ErrorSuccess || counter.Frequency <= 0 {
counter.Frequency, _ = kernel32.QueryPerformanceFrequency()
}
}
}
@@ -569,11 +570,11 @@ func (c *Collector[T]) MetricType(counterName string) (prometheus.ValueType, boo
}
// Frequency returns the time base in ticks per second of the raw timer counter
// named counterName. The raw value of a PERF_AVERAGE_TIMER counter is the sum
// of the measured durations in these ticks; divide it by the frequency to get
// seconds. It returns false if the counter is unknown, is not such a timer or
// its time base is not known yet. [Collector.Collect] reads a missing time
// base again.
// named counterName. [Collector.Collect] already converts the values of these
// counters to seconds; a collector needs the time base only to work on the
// ticks, for example to extend a 32-bit counter past its wrap. It returns
// false if the counter is unknown, is not such a timer or its time base is not
// known yet.
func (c *Collector[T]) Frequency(counterName string) (int64, bool) {
if c == nil {
return 0, false
@@ -724,7 +725,8 @@ func hasTimeBase(counterType uint32) bool {
// the collector was created. Early after a reboot, PDH can report no data
// for it, e.g. for APP_POOL_WAS while WAS is still starting (#2313). Without a
// time base, an elapsed time counter has no valid value and every instance is
// omitted.
// omitted. An average timer falls back to QueryPerformanceFrequency at
// creation, so it only needs this if that failed too.
func (c *Collector[T]) loadTimeBase(counter *Counter) {
if c.resultType != CounterTypeRaw || !hasTimeBase(counter.Type) || counter.Frequency > 0 {
return
@@ -810,9 +812,26 @@ func (c *Collector[T]) setRawValue(row *T, counter *Counter, value RawCounter) b
if counter.FieldIndexValue != -1 {
c.rows.setValue(row, counter.FieldIndexValue, float64(value.SecondValue-value.FirstValue)/float64(counter.Frequency))
}
case PERF_100NSEC_TIMER, PERF_PRECISION_100NS_TIMER:
case PERF_100NSEC_TIMER, PERF_100NSEC_TIMER_INV, PERF_100NSEC_MULTI_TIMER, PERF_100NSEC_MULTI_TIMER_INV, PERF_PRECISION_100NS_TIMER,
PERF_COUNTER_100NS_QUEUELEN_TYPE:
// The queue length type sums the queue length times the elapsed 100 ns
// ticks, so in seconds its rate is the average queue length.
if counter.FieldIndexValue != -1 {
c.rows.setValue(row, counter.FieldIndexValue, float64(value.FirstValue)*TicksToSecondScaleFactor)
c.rows.setValue(row, counter.FieldIndexValue, float64(value.FirstValue)/TicksPerSecond)
}
case PERF_AVERAGE_TIMER:
// The first value sums the measured durations in ticks of the counter's
// time base, the second value counts the operations.
if counter.Frequency <= 0 {
return false
}
if counter.FieldIndexSecondValue != -1 {
c.rows.setValue(row, counter.FieldIndexSecondValue, float64(value.SecondValue))
}
if counter.FieldIndexValue != -1 {
c.rows.setValue(row, counter.FieldIndexValue, float64(value.FirstValue)/float64(counter.Frequency))
}
default:
if counter.FieldIndexSecondValue != -1 {
+64 -27
View File
@@ -385,6 +385,32 @@ func TestSetRawValueElapsedTime(t *testing.T) {
require.True(t, c.setRawValue(&values, counter, RawCounter{FirstValue: 1, SecondValue: 2}))
}
// TestSetRawValue100nsTimers checks that every 100 ns timer type, and the
// 100 ns queue length type, is exported in seconds. 115960642 * (1 / 1e7) is
// 11.596064199999999, so the test also catches a multiplication by the
// reciprocal.
func TestSetRawValue100nsTimers(t *testing.T) {
t.Parallel()
c := newRowSetTestCollector(false)
for _, counterType := range []uint32{
PERF_100NSEC_TIMER,
PERF_100NSEC_TIMER_INV,
PERF_100NSEC_MULTI_TIMER,
PERF_100NSEC_MULTI_TIMER_INV,
PERF_PRECISION_100NS_TIMER,
PERF_COUNTER_100NS_QUEUELEN_TYPE,
} {
var values rowSetValues
counter := &Counter{Type: counterType, FieldIndexValue: int(unsafe.Offsetof(values.A)), FieldIndexSecondValue: -1}
require.True(t, c.setRawValue(&values, counter, RawCounter{FirstValue: 115_960_642, SecondValue: 1}))
require.InDelta(t, 11.5960642, values.A, 0, "counter type %#x", counterType)
}
}
// TestCollectLoadsMissingTimeBase checks that an elapsed time counter whose time
// base was not available when the collector was created gets it on a later
// sample, instead of omitting every instance until a restart (#2313).
@@ -419,17 +445,15 @@ func TestCollectLoadsMissingTimeBase(t *testing.T) {
require.Positive(t, dst[0].SystemUpTime)
}
// TestFrequencyOfAverageTimer checks that the time base of a raw average timer
// is available, also when it was missing at startup, and that other counters
// have none.
func TestFrequencyOfAverageTimer(t *testing.T) {
// TestAverageTimerInSeconds checks that the raw value of an average timer is
// converted to seconds with the counter's time base, also when the time base
// was missing at startup, and that the second value keeps the operation count.
func TestAverageTimerInSeconds(t *testing.T) {
t.Parallel()
type physicalDisk struct {
AvgDiskSecPerRead float64 `perfdata:"Avg. Disk sec/Read"`
AvgDiskSecPerReadBase float64 `perfdata:"Avg. Disk sec/Read,secondvalue"`
DiskReadsPerSec float64 `perfdata:"Disk Reads/sec"`
CurrentDiskQueueLength float64 `perfdata:"Current Disk Queue Length"`
AvgDiskSecPerRead float64 `perfdata:"Avg. Disk sec/Read"`
AvgDiskSecPerReadBase float64 `perfdata:"Avg. Disk sec/Read,secondvalue"`
}
collector, err := NewCollector[physicalDisk](slog.New(slog.DiscardHandler), CounterTypeRaw, "PhysicalDisk", []string{"_Total"})
@@ -437,36 +461,45 @@ func TestFrequencyOfAverageTimer(t *testing.T) {
t.Cleanup(collector.Close)
frequency, ok := collector.Frequency("Avg. Disk sec/Read")
require.True(t, ok)
require.Positive(t, frequency)
for _, name := range []string{"Disk Reads/sec", "Current Disk Queue Length", "unknown"} {
_, ok = collector.Frequency(name)
require.False(t, ok, name)
}
// As if GetCounterTimeBase failed at startup.
idx := slices.IndexFunc(collector.counters, func(counter Counter) bool { return counter.Name == "Avg. Disk sec/Read" })
require.NotEqual(t, -1, idx)
collector.counters[idx].Frequency = 0
frequency := collector.counters[idx].Frequency
require.Positive(t, frequency)
_, ok = collector.Frequency("Avg. Disk sec/Read")
require.False(t, ok)
// As if GetCounterTimeBase failed at startup.
collector.counters[idx].Frequency = 0
var dst []physicalDisk
require.NoError(t, collector.Collect(&dst))
require.Equal(t, frequency, collector.counters[idx].Frequency)
require.Len(t, dst, 1)
require.GreaterOrEqual(t, dst[0].AvgDiskSecPerRead, 0.0)
}
reloaded, ok := collector.Frequency("Avg. Disk sec/Read")
require.True(t, ok)
require.Equal(t, frequency, reloaded)
// TestSetRawValueAverageTimer checks the conversion of an average timer and
// that a counter without a time base has no valid value.
func TestSetRawValueAverageTimer(t *testing.T) {
t.Parallel()
var nilCollector *Collector[physicalDisk]
c := newRowSetTestCollector(false)
_, ok = nilCollector.Frequency("Avg. Disk sec/Read")
require.False(t, ok)
var values rowSetValues
counter := &Counter{
Type: PERF_AVERAGE_TIMER,
Frequency: 2_500_000,
FieldIndexValue: int(unsafe.Offsetof(values.B)),
FieldIndexSecondValue: int(unsafe.Offsetof(values.BSecond)),
}
require.True(t, c.setRawValue(&values, counter, RawCounter{FirstValue: 5_000_000, SecondValue: 7}))
require.InDelta(t, 2.0, values.B, 0)
require.InDelta(t, 7.0, values.BSecond, 0)
counter.Frequency = 0
require.False(t, c.setRawValue(&values, counter, RawCounter{FirstValue: 1, SecondValue: 1}))
}
func TestMetricTypeForCounterType(t *testing.T) {
@@ -474,6 +507,10 @@ func TestMetricTypeForCounterType(t *testing.T) {
require.Equal(t, prometheus.GaugeValue, metricType(CounterTypeRaw, PERF_COUNTER_RAWCOUNT))
require.Equal(t, prometheus.CounterValue, metricType(CounterTypeRaw, PERF_COUNTER_BULK_COUNT))
require.Equal(t, prometheus.CounterValue, metricType(CounterTypeRaw, PERF_100NSEC_MULTI_TIMER))
require.Equal(t, prometheus.CounterValue, metricType(CounterTypeRaw, PERF_100NSEC_MULTI_TIMER_INV))
require.Equal(t, prometheus.CounterValue, metricType(CounterTypeRaw, PERF_AVERAGE_TIMER))
require.Equal(t, prometheus.CounterValue, metricType(CounterTypeRaw, PERF_COUNTER_100NS_QUEUELEN_TYPE))
require.Equal(t, prometheus.GaugeValue, metricType(CounterTypeRaw, 0xFFFFFFFF))
require.Equal(t, prometheus.GaugeValue, metricType(CounterTypeFormatted, PERF_COUNTER_BULK_COUNT))
}
+26 -19
View File
@@ -21,8 +21,11 @@ import "github.com/prometheus/client_golang/prometheus"
// Conversion factors.
const (
TicksToSecondScaleFactor = 1 / 1e7
WindowsEpoch int64 = 116444736000000000
// TicksPerSecond is the number of 100 ns ticks per second. Divide by it
// rather than multiply by its reciprocal: the quotient is correctly rounded,
// the product is not.
TicksPerSecond = 1e7
WindowsEpoch int64 = 116444736000000000
)
// Based on https://github.com/leoluk/perflib_exporter/blob/master/collector/mapper.go
@@ -74,21 +77,25 @@ const (
//nolint:gochecknoglobals
var SupportedCounterTypes = map[uint32]prometheus.ValueType{
PERF_COUNTER_RAWCOUNT_HEX: prometheus.GaugeValue,
PERF_COUNTER_LARGE_RAWCOUNT_HEX: prometheus.GaugeValue,
PERF_COUNTER_RAWCOUNT: prometheus.GaugeValue,
PERF_COUNTER_LARGE_RAWCOUNT: prometheus.GaugeValue,
PERF_COUNTER_DELTA: prometheus.CounterValue,
PERF_COUNTER_COUNTER: prometheus.CounterValue,
PERF_COUNTER_BULK_COUNT: prometheus.CounterValue,
PERF_RAW_FRACTION: prometheus.GaugeValue,
PERF_LARGE_RAW_FRACTION: prometheus.GaugeValue,
PERF_100NSEC_TIMER: prometheus.CounterValue,
PERF_PRECISION_100NS_TIMER: prometheus.CounterValue,
PERF_SAMPLE_FRACTION: prometheus.GaugeValue,
PERF_100NSEC_TIMER_INV: prometheus.CounterValue,
PERF_ELAPSED_TIME: prometheus.GaugeValue,
PERF_SAMPLE_BASE: prometheus.GaugeValue,
PERF_RAW_BASE: prometheus.GaugeValue,
PERF_LARGE_RAW_BASE: prometheus.GaugeValue,
PERF_COUNTER_RAWCOUNT_HEX: prometheus.GaugeValue,
PERF_COUNTER_LARGE_RAWCOUNT_HEX: prometheus.GaugeValue,
PERF_COUNTER_RAWCOUNT: prometheus.GaugeValue,
PERF_COUNTER_LARGE_RAWCOUNT: prometheus.GaugeValue,
PERF_COUNTER_DELTA: prometheus.CounterValue,
PERF_COUNTER_COUNTER: prometheus.CounterValue,
PERF_COUNTER_BULK_COUNT: prometheus.CounterValue,
PERF_RAW_FRACTION: prometheus.GaugeValue,
PERF_LARGE_RAW_FRACTION: prometheus.GaugeValue,
PERF_100NSEC_TIMER: prometheus.CounterValue,
PERF_PRECISION_100NS_TIMER: prometheus.CounterValue,
PERF_SAMPLE_FRACTION: prometheus.GaugeValue,
PERF_100NSEC_TIMER_INV: prometheus.CounterValue,
PERF_100NSEC_MULTI_TIMER: prometheus.CounterValue,
PERF_100NSEC_MULTI_TIMER_INV: prometheus.CounterValue,
PERF_ELAPSED_TIME: prometheus.GaugeValue,
PERF_AVERAGE_TIMER: prometheus.CounterValue,
PERF_COUNTER_100NS_QUEUELEN_TYPE: prometheus.CounterValue,
PERF_SAMPLE_BASE: prometheus.GaugeValue,
PERF_RAW_BASE: prometheus.GaugeValue,
PERF_LARGE_RAW_BASE: prometheus.GaugeValue,
}