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
+3 -4
View File
@@ -21,10 +21,9 @@ one.
`PERF_AVERAGE_TIMER` (`AverageTimer32`). The raw value is the summed
duration in ticks of the counter's time base (`PdhGetCounterTimeBase`,
10 MHz on the test host, same as `QueryPerformanceFrequency`); the base
(`,secondvalue`) is the number of operations. `pdh.CounterTypeRaw` does not
convert this type; `internal/pdh` reads its time base, and the collector
divides the raw value by `pdh.Collector.Frequency` to get seconds. Do not
hardcode a scale factor.
(`,secondvalue`) is the number of operations. `internal/pdh` divides the
raw value by the time base, so the collector gets seconds. Do not scale it
again.
- Sample after about one hour: `Average Config Database Query Time` raw
11521176, base 306, which is 1.152 s in total, 3.8 ms per query.
`Federation Metadata Requests` was 1, all authentication counters 0.
+17 -23
View File
@@ -409,19 +409,13 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
return fmt.Errorf("failed to collect ADFS metrics: %w", types.ErrNoDataUnexpected)
}
// The query times are PERF_AVERAGE_TIMER counters: the raw value is the
// summed duration in ticks of the counter's time base.
artifactDBFrequency, _ := c.perfDataCollector.Frequency(counterArtifactDBQueryTime)
configDBFrequency, _ := c.perfDataCollector.Frequency(counterConfigDBQueryTime)
c.collectMetrics(ch, c.perfDataObject[0], artifactDBFrequency, configDBFrequency)
c.collectMetrics(ch, c.perfDataObject[0])
return nil
}
// collectMetrics sends the metrics of data. A query time is only sent if the
// frequency of its counter is known.
func (c *Collector) collectMetrics(ch chan<- prometheus.Metric, data perfDataCounterValues, artifactDBFrequency, configDBFrequency int64) {
// collectMetrics sends the metrics of data.
func (c *Collector) collectMetrics(ch chan<- prometheus.Metric, data perfDataCounterValues) {
ch <- prometheus.MustNewConstMetric(
c.adLoginConnectionFailures,
prometheus.CounterValue,
@@ -662,13 +656,13 @@ func (c *Collector) collectMetrics(ch chan<- prometheus.Metric, data perfDataCou
data.ArtifactDBQueries,
)
if artifactDBFrequency > 0 {
ch <- prometheus.MustNewConstMetric(
c.artifactDBQueryTime,
prometheus.CounterValue,
data.ArtifactDBQueryTimeTicks/float64(artifactDBFrequency),
)
}
// PERF_AVERAGE_TIMER: the summed query time, converted to seconds by
// internal/pdh; the second value counts the queries.
ch <- prometheus.MustNewConstMetric(
c.artifactDBQueryTime,
prometheus.CounterValue,
data.ArtifactDBQueryTimeSeconds,
)
ch <- prometheus.MustNewConstMetric(
c.configDBFailures,
@@ -682,13 +676,13 @@ func (c *Collector) collectMetrics(ch chan<- prometheus.Metric, data perfDataCou
data.ConfigDBQueries,
)
if configDBFrequency > 0 {
ch <- prometheus.MustNewConstMetric(
c.configDBQueryTime,
prometheus.CounterValue,
data.ConfigDBQueryTimeTicks/float64(configDBFrequency),
)
}
// PERF_AVERAGE_TIMER: the summed query time, converted to seconds by
// internal/pdh; the second value counts the queries.
ch <- prometheus.MustNewConstMetric(
c.configDBQueryTime,
prometheus.CounterValue,
data.ConfigDBQueryTimeSeconds,
)
ch <- prometheus.MustNewConstMetric(
c.federationMetadataRequests,
+31 -80
View File
@@ -25,100 +25,51 @@ import (
"github.com/stretchr/testify/require"
)
// TestCollectDatabaseQueryTimes checks that the raw ticks of the database
// query time counters are converted to seconds with the counter's time base,
// and that the query counts are exported.
// TestCollectDatabaseQueryTimes checks that the database query times, which
// internal/pdh converts to seconds, and the query counts are exported.
func TestCollectDatabaseQueryTimes(t *testing.T) {
t.Parallel()
data := perfDataCounterValues{
ArtifactDBQueryTimeTicks: 2_500_000,
ArtifactDBQueries: 5,
ConfigDBQueryTimeTicks: 11_521_176,
ConfigDBQueries: 306,
ArtifactDBQueryTimeSeconds: 0.25,
ArtifactDBQueries: 5,
ConfigDBQueryTimeSeconds: 1.1521176,
ConfigDBQueries: 306,
}
for _, tc := range []struct {
name string
artifactDBFrequency int64
configDBFrequency int64
expected map[string]float64
absent []string
}{
{
name: "10 MHz",
artifactDBFrequency: 10_000_000,
configDBFrequency: 10_000_000,
expected: map[string]float64{
"windows_adfs_db_artifact_query_time_seconds_total": 0.25,
"windows_adfs_db_artifact_queries_total": 5,
"windows_adfs_db_config_query_time_seconds_total": 1.1521176,
"windows_adfs_db_config_queries_total": 306,
},
},
{
name: "other frequency",
artifactDBFrequency: 2_500_000,
configDBFrequency: 3_000_000,
expected: map[string]float64{
"windows_adfs_db_artifact_query_time_seconds_total": 1,
"windows_adfs_db_config_query_time_seconds_total": 3.840392,
},
},
{
name: "unknown frequency",
expected: map[string]float64{
"windows_adfs_db_artifact_queries_total": 5,
"windows_adfs_db_config_queries_total": 306,
},
absent: []string{
"windows_adfs_db_artifact_query_time_seconds_total",
"windows_adfs_db_config_query_time_seconds_total",
},
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
expected := map[string]float64{
"windows_adfs_db_artifact_query_time_seconds_total": 0.25,
"windows_adfs_db_artifact_queries_total": 5,
"windows_adfs_db_config_query_time_seconds_total": 1.1521176,
"windows_adfs_db_config_queries_total": 306,
}
c := New(nil)
c.buildDescriptors()
c := New(nil)
c.buildDescriptors()
registry := prometheus.NewPedanticRegistry()
require.NoError(t, registry.Register(metricCollector{
collector: c,
data: data,
artifactDBFrequency: tc.artifactDBFrequency,
configDBFrequency: tc.configDBFrequency,
}))
registry := prometheus.NewPedanticRegistry()
require.NoError(t, registry.Register(metricCollector{collector: c, data: data}))
families, err := registry.Gather()
require.NoError(t, err)
families, err := registry.Gather()
require.NoError(t, err)
byName := make(map[string]*dto.MetricFamily, len(families))
for _, family := range families {
byName[family.GetName()] = family
}
byName := make(map[string]*dto.MetricFamily, len(families))
for _, family := range families {
byName[family.GetName()] = family
}
for name, value := range tc.expected {
family, ok := byName[name]
require.True(t, ok, "missing metric %s", name)
require.Equal(t, dto.MetricType_COUNTER, family.GetType(), name)
require.Len(t, family.GetMetric(), 1, name)
require.InDelta(t, value, family.GetMetric()[0].GetCounter().GetValue(), 1e-9, name)
}
for _, name := range tc.absent {
require.NotContains(t, byName, name)
}
})
for name, value := range expected {
family, ok := byName[name]
require.True(t, ok, "missing metric %s", name)
require.Equal(t, dto.MetricType_COUNTER, family.GetType(), name)
require.Len(t, family.GetMetric(), 1, name)
require.InDelta(t, value, family.GetMetric()[0].GetCounter().GetValue(), 0, name)
}
}
type metricCollector struct {
collector *Collector
data perfDataCounterValues
artifactDBFrequency int64
configDBFrequency int64
collector *Collector
data perfDataCounterValues
}
func (c metricCollector) Describe(ch chan<- *prometheus.Desc) {
@@ -126,5 +77,5 @@ func (c metricCollector) Describe(ch chan<- *prometheus.Desc) {
}
func (c metricCollector) Collect(ch chan<- prometheus.Metric) {
c.collector.collectMetrics(ch, c.data, c.artifactDBFrequency, c.configDBFrequency)
c.collector.collectMetrics(ch, c.data)
}
+2 -9
View File
@@ -17,19 +17,12 @@
package adfs
// PERF_AVERAGE_TIMER counters. The raw value is the summed query time in ticks
// of the counter's time base, the second value the number of queries.
const (
counterArtifactDBQueryTime = "Average Artifact Database Query Time"
counterConfigDBQueryTime = "Average Config Database Query Time"
)
type perfDataCounterValues struct {
AdLoginConnectionFailures float64 `perfdata:"AD Login Connection Failures"`
ArtifactDBFailures float64 `perfdata:"Artifact Database Connection Failures"`
ArtifactDBQueryTimeTicks float64 `perfdata:"Average Artifact Database Query Time"`
ArtifactDBQueryTimeSeconds float64 `perfdata:"Average Artifact Database Query Time"`
ArtifactDBQueries float64 `perfdata:"Average Artifact Database Query Time,secondvalue"`
ConfigDBQueryTimeTicks float64 `perfdata:"Average Config Database Query Time"`
ConfigDBQueryTimeSeconds float64 `perfdata:"Average Config Database Query Time"`
ConfigDBQueries float64 `perfdata:"Average Config Database Query Time,secondvalue"`
CertificateAuthentications float64 `perfdata:"Certificate Authentications"`
ConfigDBFailures float64 `perfdata:"Configuration Database Connection Failures"`
+6 -6
View File
@@ -671,7 +671,7 @@ func (c *Collector) collectHCSContainer(ch chan<- prometheus.Metric, containerDe
ch <- prometheus.MustNewConstMetric(
c.runtimeTotal,
prometheus.CounterValue,
float64(containerStats.Processor.TotalRuntime100ns)*pdh.TicksToSecondScaleFactor,
float64(containerStats.Processor.TotalRuntime100ns)/pdh.TicksPerSecond,
containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container,
)
@@ -679,7 +679,7 @@ func (c *Collector) collectHCSContainer(ch chan<- prometheus.Metric, containerDe
ch <- prometheus.MustNewConstMetric(
c.runtimeUser,
prometheus.CounterValue,
float64(containerStats.Processor.RuntimeUser100ns)*pdh.TicksToSecondScaleFactor,
float64(containerStats.Processor.RuntimeUser100ns)/pdh.TicksPerSecond,
containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container,
)
@@ -687,7 +687,7 @@ func (c *Collector) collectHCSContainer(ch chan<- prometheus.Metric, containerDe
ch <- prometheus.MustNewConstMetric(
c.runtimeKernel,
prometheus.CounterValue,
float64(containerStats.Processor.RuntimeKernel100ns)*pdh.TicksToSecondScaleFactor,
float64(containerStats.Processor.RuntimeKernel100ns)/pdh.TicksPerSecond,
containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container,
)
@@ -939,7 +939,7 @@ func (c *Collector) collectJobContainer(ch chan<- prometheus.Metric, containerID
ch <- prometheus.MustNewConstMetric(
c.runtimeTotal,
prometheus.CounterValue,
(float64(jobInfo.BasicInfo.TotalKernelTime)+float64(jobInfo.BasicInfo.TotalUserTime))*pdh.TicksToSecondScaleFactor,
(float64(jobInfo.BasicInfo.TotalKernelTime)+float64(jobInfo.BasicInfo.TotalUserTime))/pdh.TicksPerSecond,
containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container,
)
@@ -947,7 +947,7 @@ func (c *Collector) collectJobContainer(ch chan<- prometheus.Metric, containerID
ch <- prometheus.MustNewConstMetric(
c.runtimeUser,
prometheus.CounterValue,
float64(jobInfo.BasicInfo.TotalUserTime)*pdh.TicksToSecondScaleFactor,
float64(jobInfo.BasicInfo.TotalUserTime)/pdh.TicksPerSecond,
containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container,
)
@@ -955,7 +955,7 @@ func (c *Collector) collectJobContainer(ch chan<- prometheus.Metric, containerID
ch <- prometheus.MustNewConstMetric(
c.runtimeKernel,
prometheus.CounterValue,
float64(jobInfo.BasicInfo.TotalKernelTime)*pdh.TicksToSecondScaleFactor,
float64(jobInfo.BasicInfo.TotalKernelTime)/pdh.TicksPerSecond,
containerInfo.id, containerInfo.namespace, containerInfo.pod, containerInfo.container,
)
+3 -3
View File
@@ -18,9 +18,9 @@ Hyper-V guest with 12 logical processors in one processor group.
so `Collect` holds the previous value of `time_seconds_total` and
`cstate_seconds_total` for drops below `maxTimeDrop` (1 s).
- `% Processor Time` and `% Priority Time` are `PERF_100NSEC_TIMER_INV`. The
raw value is cumulative idle (non-busy) time in 100 ns ticks, not busy time,
and `internal/pdh` does not scale this type to seconds. In the sample, the
raw value of `% Processor Time` equals `% Idle Time`.
raw value is cumulative idle (non-busy) time in 100 ns ticks, not busy time.
`internal/pdh` scales it to seconds like the other 100 ns timers. In the
sample, the raw value of `% Processor Time` equals `% Idle Time`.
- `% Processor Utility`, `% Processor Performance` and `% Privileged Utility`
are `PERF_AVERAGE_BULK`. Raw and base are both cumulative. The displayed
value is delta raw / delta base and is already a percentage (it can exceed
+1 -1
View File
@@ -512,7 +512,7 @@ func (c *Collector) collectGpuEngineMetrics(ch chan<- prometheus.Metric, scrape
ch <- scrape.MustNewConstMetric(
c.gpuEngineRunningTime,
prometheus.CounterValue,
data.RunningTime/10_000_000,
data.RunningTime/pdh.TicksPerSecond,
instance.Pid, instance.Luid, device.ID, instance.Phys, instance.Eng, instance.Engtype,
)
}
@@ -205,19 +205,20 @@ func (c *Collector) publishVirtualStorageDevice(ch chan<- prometheus.Metric, scr
data.Name,
)
// The raw value of a PERF_AVERAGE_TIMER counter is the sum of all IO latencies in
// 100ns ticks; the operation count it is averaged over is Throughput.
// The value of a PERF_AVERAGE_TIMER counter is the sum of all IO latencies,
// converted to seconds by internal/pdh; the operation count it is averaged
// over is Throughput.
ch <- scrape.MustNewConstMetric(
c.virtualStorageDeviceIOLatency,
prometheus.CounterValue,
data.VirtualStorageDeviceLatency*pdh.TicksToSecondScaleFactor,
data.VirtualStorageDeviceLatency,
data.Name,
)
ch <- scrape.MustNewConstMetric(
c.virtualStorageDeviceLowerIOLatency,
prometheus.CounterValue,
data.VirtualStorageDeviceLowerLatency*pdh.TicksToSecondScaleFactor,
data.VirtualStorageDeviceLowerLatency,
data.Name,
)
}
+3 -5
View File
@@ -19,11 +19,9 @@ multi-instance. These notes were recorded on a Windows Server 2025
were all 9858832; the write counters were all 11319205, and
`Avg. Disk sec/Transfer` (21178037) is their sum. The raw value is the
cumulative busy time in 100 ns ticks. `internal/pdh` converts the
`PERF_PRECISION_100NS_TIMER` counters to seconds. The queue length and
`PERF_AVERAGE_TIMER` values arrive in ticks and need the 1e-7 factor; the
counter time base is 10 MHz here. The queue length counters duplicate the
busy time, so the collector no longer reads them; the average queue length is
`rate(windows_logical_disk_{read,write}_seconds_total[5m])`.
`PERF_PRECISION_100NS_TIMER` counters to seconds. The queue length counters
duplicate the busy time, so the collector no longer reads them; the average
queue length is `rate(windows_logical_disk_{read,write}_seconds_total[5m])`.
- `Avg. Disk sec/Read`, `/Write` and `/Transfer` are `PERF_AVERAGE_TIMER`.
Their base is the operation count and equals `Disk Reads/sec` (3073),
`Disk Writes/sec` (1698) and their sum (4771). Their raw values duplicate the
+2 -2
View File
@@ -17,8 +17,8 @@ multi-instance. These notes were recorded on a Windows Server 2025
value is the cumulative busy time in 100 ns ticks.
- `internal/pdh` converts the `PERF_PRECISION_100NS_TIMER` counters
(`% Disk Read Time`, `% Disk Write Time`, `% Idle Time`) to seconds.
`PERF_AVERAGE_TIMER` values arrive in ticks of the 10 MHz counter time base
and need the 1e-7 factor.
It also converts the `PERF_AVERAGE_TIMER` values (`Avg. Disk sec/*`) to
seconds with the counter time base, which was 10 MHz on JOK-PC.
- The second raw value of `% Idle Time` is the sample time as a UTC FILETIME;
PDH divides by its delta. Even with that divisor the idle delta of a busy
disk exceeded the elapsed time by up to 43% between 1 s samples on a
@@ -295,21 +295,21 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
ch <- prometheus.MustNewConstMetric(
c.readLatency,
prometheus.CounterValue,
data.AvgDiskSecPerRead*pdh.TicksToSecondScaleFactor,
data.AvgDiskSecPerRead,
disk_number,
)
ch <- prometheus.MustNewConstMetric(
c.writeLatency,
prometheus.CounterValue,
data.AvgDiskSecPerWrite*pdh.TicksToSecondScaleFactor,
data.AvgDiskSecPerWrite,
disk_number,
)
ch <- prometheus.MustNewConstMetric(
c.readWriteLatency,
prometheus.CounterValue,
data.AvgDiskSecPerTransfer*pdh.TicksToSecondScaleFactor,
data.AvgDiskSecPerTransfer,
disk_number,
)
}
+5 -6
View File
@@ -42,11 +42,10 @@ const Name = "process"
const (
// windowsEpoch is the FILETIME of the Unix epoch, in 100ns intervals since 1601.
windowsEpoch int64 = 116444736000000000
// ticksPerSecond is the number of 100ns intervals per second.
// ticksPerSecond is the number of 100ns intervals per second. Dividing by it,
// like internal/pdh does, keeps the CPU time values bit-identical to the
// Process counter set.
ticksPerSecond = 1e7
// secondsPerTick converts 100ns intervals to seconds. Multiplying by it, like the perflib
// reader did, keeps the CPU time values bit-identical to the Process counter set.
secondsPerTick = 1 / ticksPerSecond
)
type Config struct {
@@ -486,14 +485,14 @@ func (c *Collector) collectProcess(ch chan<- prometheus.Metric, scrape *labelcac
ch <- scrape.MustNewConstMetric(
c.cpuTimeTotal,
prometheus.CounterValue,
float64(process.kernelTime)*secondsPerTick,
float64(process.kernelTime)/ticksPerSecond,
name, pid, "privileged",
)
ch <- scrape.MustNewConstMetric(
c.cpuTimeTotal,
prometheus.CounterValue,
float64(process.userTime)*secondsPerTick,
float64(process.userTime)/ticksPerSecond,
name, pid, "user",
)
+9 -5
View File
@@ -13,12 +13,16 @@ host has connected to. These notes were recorded on a Windows Server 2025
- `Current Data Queue Length` is the only gauge ("the current number of read
or write requests outstanding"). The `/sec` counters are cumulative counts.
- `Avg. Data/Read/Write Queue Length` are `PERF_COUNTER_100NS_QUEUELEN_TYPE`:
the raw value is accumulated queue time in 100 ns ticks. `internal/pdh` does
not convert this type, so the collector applies the 1e-7 factor.
the raw value is accumulated queue time in 100 ns ticks, which
`internal/pdh` converts to seconds.
- `Avg. sec/Read`, `Avg. sec/Write` and `Avg. sec/Data Request` are
`PERF_AVERAGE_TIMER`. The first value is the summed latency in ticks of the
10 MHz counter time base and the base is the request count; the collector
reads only the first value.
`PERF_AVERAGE_TIMER`. The first value is the summed latency, which
`internal/pdh` converts to seconds with the counter time base, and the base
is the request count; the collector reads only the first value. The raw
sum is 32 bits, so the collector converts it back to ticks with
`pdh.Collector.Frequency`, extends it past the 2^32 wrap with
`utils.Counter32` and divides again. Keep that round trip: extending the
seconds directly truncates them and adds 2^32 seconds per wrap.
- `Data Bytes/sec`, `Read Bytes/sec` and `Write Bytes/sec` are bytes
(`PERF_COUNTER_BULK_COUNT`).
+43 -13
View File
@@ -20,6 +20,7 @@ package smbclient
import (
"fmt"
"log/slog"
"math"
"strings"
"time"
@@ -111,6 +112,12 @@ func restartSignals(data *perfDataCounterValues) [numRestartSignals]float64 {
}
}
// timerCounters32 names the PERF_AVERAGE_TIMER counters in the order of
// fields32.
//
//nolint:gochecknoglobals
var timerCounters32 = [numTimers32]string{"Avg. sec/Read", "Avg. sec/Write", "Avg. sec/Data Request"}
// fields32 returns the 32-bit counters: first the PERF_AVERAGE_TIMER
// numerators, then the request counters (PERF_COUNTER_COUNTER).
func fields32(data *perfDataCounterValues) [numTimers32 + numCounts32]*float64 {
@@ -275,6 +282,16 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
c.generation++
// internal/pdh converts the timer sums to seconds; extendCounters32 needs
// their ticks.
var frequencies [numTimers32]float64
for i, name := range timerCounters32 {
if frequency, ok := c.perfDataCollector.Frequency(name); ok {
frequencies[i] = float64(frequency)
}
}
for _, data := range c.perfDataObject {
serverValue, shareValue, ok := parseInstanceName(data.Name)
if !ok {
@@ -289,50 +306,50 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
continue
}
c.extendCounters32(&data)
c.extendCounters32(&data, frequencies)
// Request time spent on queue. Convert from ticks to seconds.
// Request time spent on queue, in seconds.
ch <- prometheus.MustNewConstMetric(
c.requestQueueSecsTotal,
prometheus.CounterValue,
data.AvgDataQueueLength*pdh.TicksToSecondScaleFactor,
data.AvgDataQueueLength,
serverValue, shareValue,
)
// Read time spent on queue. Convert from ticks to seconds.
// Read time spent on queue, in seconds.
ch <- prometheus.MustNewConstMetric(
c.readRequestQueueSecsTotal,
prometheus.CounterValue,
data.AvgReadQueueLength*pdh.TicksToSecondScaleFactor,
data.AvgReadQueueLength,
serverValue, shareValue,
)
ch <- prometheus.MustNewConstMetric(
c.readSecsTotal,
prometheus.CounterValue,
data.AvgSecPerRead*pdh.TicksToSecondScaleFactor,
data.AvgSecPerRead,
serverValue, shareValue,
)
ch <- prometheus.MustNewConstMetric(
c.writeSecsTotal,
prometheus.CounterValue,
data.AvgSecPerWrite*pdh.TicksToSecondScaleFactor,
data.AvgSecPerWrite,
serverValue, shareValue,
)
ch <- prometheus.MustNewConstMetric(
c.requestSecs,
prometheus.CounterValue,
data.AvgSecPerDataRequest*pdh.TicksToSecondScaleFactor,
data.AvgSecPerDataRequest,
serverValue, shareValue,
)
// Write time spent on queue. Convert from ticks to seconds.
// Write time spent on queue, in seconds.
ch <- prometheus.MustNewConstMetric(
c.writeRequestQueueSecsTotal,
prometheus.CounterValue,
data.AvgWriteQueueLength*pdh.TicksToSecondScaleFactor,
data.AvgWriteQueueLength,
serverValue, shareValue,
)
@@ -458,11 +475,18 @@ func (c *Collector) pruneCounters32() {
}
}
// extendCounters32 replaces the raw 32-bit counters with values that keep
// counting past 2^32.
func (c *Collector) extendCounters32(data *perfDataCounterValues) {
// extendCounters32 replaces the 32-bit counters with values that keep
// counting past 2^32. The timer sums arrive in seconds and wrap at 2^32 ticks
// of their time base in frequencies, so they are extended as ticks.
func (c *Collector) extendCounters32(data *perfDataCounterValues, frequencies [numTimers32]float64) {
fields := fields32(data)
for i, frequency := range frequencies {
if frequency > 0 {
*fields[i] = math.Round(*fields[i] * frequency)
}
}
signals := restartSignals(data)
counters, ok := c.counters32[data.Name]
@@ -493,6 +517,12 @@ func (c *Collector) extendCounters32(data *perfDataCounterValues) {
for i, field := range fields {
*field = counters.counters[i].Update(*field, reset)
}
for i, frequency := range frequencies {
if frequency > 0 {
*fields[i] /= frequency
}
}
}
// parseInstanceName splits an SMB Client Shares instance name such as
@@ -53,29 +53,36 @@ func TestExtendCounters32(t *testing.T) {
c := &Collector{counters32: make(map[string]*shareCounters32)}
// internal/pdh delivers the timer sums in seconds of a 10 MHz time base.
frequencies := [numTimers32]float64{1e7, 1e7, 1e7}
sample := func(data perfDataCounterValues) perfDataCounterValues {
c.generation++
data.Name = `\\server\share`
c.extendCounters32(&data)
c.extendCounters32(&data, frequencies)
c.pruneCounters32()
return data
}
got := sample(perfDataCounterValues{AvgSecPerRead: 1 << 20, ReadRequestsPerSec: 7, ReadBytesPerSec: 100})
require.Equal(t, float64(1<<20), got.AvgSecPerRead) //nolint:testifylint
require.Equal(t, 7.0, got.ReadRequestsPerSec) //nolint:testifylint
got := sample(perfDataCounterValues{AvgSecPerRead: (1 << 20) / 1e7, ReadRequestsPerSec: 7, ReadBytesPerSec: 100})
require.Equal(t, (1<<20)/1e7, got.AvgSecPerRead) //nolint:testifylint
require.Equal(t, 7.0, got.ReadRequestsPerSec) //nolint:testifylint
// A timer sum can wrap from anywhere: 2^32 ticks are about 430 s of
// summed request time.
got = sample(perfDataCounterValues{AvgSecPerRead: 1 << 10, ReadRequestsPerSec: 9, ReadBytesPerSec: 200})
require.Equal(t, float64(math.MaxUint32)+1+(1<<10), got.AvgSecPerRead) //nolint:testifylint
require.Equal(t, 9.0, got.ReadRequestsPerSec) //nolint:testifylint
got = sample(perfDataCounterValues{AvgSecPerRead: (1 << 10) / 1e7, ReadRequestsPerSec: 9, ReadBytesPerSec: 200})
require.Equal(t, (float64(math.MaxUint32)+1+(1<<10))/1e7, got.AvgSecPerRead) //nolint:testifylint
require.Equal(t, 9.0, got.ReadRequestsPerSec) //nolint:testifylint
// Fractions of a second survive the round trip through ticks.
got = sample(perfDataCounterValues{AvgSecPerRead: 0.1234567, ReadRequestsPerSec: 10, ReadBytesPerSec: 300})
require.Equal(t, (float64(math.MaxUint32)+1+1234567)/1e7, got.AvgSecPerRead) //nolint:testifylint
// A drop of a 64-bit byte counter shows a reconnect, even if DataBytes grew.
got = sample(perfDataCounterValues{AvgSecPerRead: 5, ReadRequestsPerSec: 1, ReadBytesPerSec: 50, DataBytesPerSec: 1000})
require.Equal(t, 5.0, got.AvgSecPerRead) //nolint:testifylint
got = sample(perfDataCounterValues{AvgSecPerRead: 5 / 1e7, ReadRequestsPerSec: 1, ReadBytesPerSec: 50, DataBytesPerSec: 1000})
require.Equal(t, 5/1e7, got.AvgSecPerRead) //nolint:testifylint
require.Equal(t, 1.0, got.ReadRequestsPerSec) //nolint:testifylint
// A metadata-only share has no byte counter to show the reconnect; a
+16
View File
@@ -32,6 +32,7 @@ var (
procKernelLocalFileTimeToFileTime = modkernel32.NewProc("LocalFileTimeToFileTime")
procOpenJobObject = modkernel32.NewProc("OpenJobObjectW")
procIsProcessInJob = modkernel32.NewProc("IsProcessInJob")
procQueryPerformanceFrequency = modkernel32.NewProc("QueryPerformanceFrequency")
// GetDiskSpaceInformationW is optional and checked by GetDiskSpaceInformation.
load = lazyproc.Once(modkernel32,
@@ -39,6 +40,7 @@ var (
procKernelLocalFileTimeToFileTime,
procOpenJobObject,
procIsProcessInJob,
procQueryPerformanceFrequency,
)
)
@@ -94,3 +96,17 @@ func LocalFileTimeToFileTime(localFileTime, utcFileTime *windows.Filetime) uint3
return uint32(ret)
}
// QueryPerformanceFrequency returns the frequency of the performance counter
// in counts per second.
// 📑 https://learn.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancefrequency
func QueryPerformanceFrequency() (int64, error) {
var frequency int64
ret, _, err := procQueryPerformanceFrequency.Call(uintptr(unsafe.Pointer(&frequency)))
if ret == 0 {
return 0, err
}
return frequency, nil
}
+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,
}