diff --git a/AGENTS.md b/AGENTS.md index 33ebb4c0..d95bb268 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -141,10 +141,11 @@ exercised a live Windows role. appropriate. Raw PDH values can be cumulative numerators, ticks or counts, even when the Windows counter name says average, percentage or seconds. Convert units explicitly; expose the base/operation count needed for ratios. - The raw value of a `PERF_AVERAGE_TIMER` counter is in ticks of its time - base: in new code, divide it by `pdh.Collector.Frequency` instead of a fixed - factor. The disk and `smbclient` collectors still use the fixed 1e-7 - (10 MHz) factor. + With `pdh.CounterTypeRaw`, `internal/pdh` converts the time types to seconds: + the 100 ns timers and `PERF_COUNTER_100NS_QUEUELEN_TYPE` by dividing by + `pdh.TicksPerSecond`, and `PERF_AVERAGE_TIMER` by dividing by the counter's + own time base (its `,secondvalue` stays the operation count). Don't scale + these again in a collector. Never publish an invalid sample as zero: that can look like a counter reset. - Keep the typed PDH collectors and `perfdata` tags. `internal/pdh` passes instance names through unchanged, so a `#N` in a name always comes from diff --git a/docs/collector.hyperv.md b/docs/collector.hyperv.md index e1bb14ce..5e379372 100644 --- a/docs/collector.hyperv.md +++ b/docs/collector.hyperv.md @@ -369,7 +369,7 @@ The `vm` label holds the instance name of the `Hyper-V Virtual SMB` performance The `Latency`, `Lower Latency`, `Queue Length` and `Lower Queue Length` performance counters are averages that Windows computes from two samples. Their raw values only grow: the sum of -all IO latencies in 100ns ticks, and the raw queue length values are the same as the raw +all IO latencies in ticks of the counter time base (usually 10 MHz), and the raw queue length values are the same as the raw latency values. windows_exporter publishes them as the `*_io_latency_seconds_total` counters: ```promql diff --git a/docs/collector.performancecounter.md b/docs/collector.performancecounter.md index 6ed78c3a..00dfcf63 100644 --- a/docs/collector.performancecounter.md +++ b/docs/collector.performancecounter.md @@ -218,16 +218,24 @@ keep counting up. If the multiplier is the reciprocal of an integer, like `0.001 the value is divided by that integer instead, which avoids rounding artifacts such as `0.009000000000000001`. If not specified, the value is exported as is. -With the object `type` `raw`, the default, some counters are already converted -to seconds, so do not scale them again: `PERF_100NSEC_TIMER` and -`PERF_PRECISION_100NS_TIMER` counters such as `% Idle Time` are exported in -seconds, and `PERF_ELAPSED_TIME` counters such as `Process\Elapsed Time` as -seconds since the start. `PERF_100NSEC_TIMER_INV` counters such as -`% Processor Time` are not converted and arrive in 100 ns ticks of inactive time -(for `% Processor Time`, the idle time); a multiplier of `1e-7` converts them to -seconds. `PERF_AVERAGE_TIMER` counters such as `Avg. Disk sec/Read` count ticks -of the performance timer, whose frequency depends on the host, so a fixed -multiplier does not fit them; use the `formatted` type for them. +With the object `type` `raw`, the default, time counters are already converted +to seconds, so do not scale them again: + +- the 100 ns timers (`PERF_100NSEC_TIMER`, `PERF_100NSEC_TIMER_INV`, + `PERF_100NSEC_MULTI_TIMER`, `PERF_100NSEC_MULTI_TIMER_INV` and + `PERF_PRECISION_100NS_TIMER`), such as `% Idle Time`; +- `PERF_COUNTER_100NS_QUEUELEN_TYPE` counters, such as `Avg. Data Queue Length` + of `SMB Client Shares`, as queue length times seconds, so their rate is the + average queue length; +- `PERF_AVERAGE_TIMER` counters, such as `Avg. Disk sec/Read`, as the summed + duration in seconds, using the time base of the counter. Their second value + is the number of operations; +- `PERF_ELAPSED_TIME` counters, such as `Process\Elapsed Time`, as seconds since + the start. + +The raw value of an `_INV` counter is the inactive time: for `% Processor Time`, +that is the idle time, not the busy time. `MULTI_TIMER` values are summed over +all units of the counter, so they can grow faster than wall time. This key is optional. diff --git a/docs/upgrade/0.32.md b/docs/upgrade/0.32.md index f9076f1b..aab18db2 100644 --- a/docs/upgrade/0.32.md +++ b/docs/upgrade/0.32.md @@ -931,6 +931,9 @@ These metrics keep their names, but 0.31 reported wrong values. Graphs show a st #### performancecounter and PDH-based collectors - In the `performancecounter` collector, counters without an explicit `type` now always get the type of the underlying Windows counter. In 0.31 a mixed object could report counters as gauges, or the other way around, and the type could change between scrapes. If Prometheus scrapes with OpenMetrics, a counter that 0.31 reported as a gauge is now stored with a `_total` suffix, which the compatibility rules can't cover. +- Raw `PERF_100NSEC_TIMER_INV`, `PERF_100NSEC_MULTI_TIMER` and `PERF_100NSEC_MULTI_TIMER_INV` counters, such as `% Processor Time` read with the `raw` type, are exported in seconds, like the other 100 ns timers. In 0.31 they were exported in 100 ns ticks, so values read this way are 10,000,000 times smaller; remove any query that divided them by `1e7`. The raw value of an `_INV` counter such as `% Processor Time` is the inactive (idle) time, not the busy time; `PERF_100NSEC_MULTI_TIMER(_INV)` values are summed over all units of the counter. +- Raw `PERF_AVERAGE_TIMER` counters, such as `Avg. Disk sec/Read`, and raw `PERF_COUNTER_100NS_QUEUELEN_TYPE` counters, such as `Avg. Data Queue Length`, read with the `performancecounter` collector are exported in seconds and as counters. In 0.31 they were exported in ticks and as gauges. `PERF_AVERAGE_TIMER` uses the time base of the counter, which is 10 MHz on most hosts. The latencies of the `physical_disk`, `smbclient` and `hyperv` collectors now use it too; on a host with another time base, 0.31 reported them wrong. +- Values converted from 100 ns ticks to seconds, in the `performancecounter`, `cpu`, `process`, `physical_disk`, `logical_disk`, `smbclient`, `container` and `hyperv` collectors, are now rounded correctly and can differ from 0.31 in the last digit. A counter that doesn't move across the upgrade can drop by that last digit once, which `rate()` and `increase()` count as a reset for one window. - Instances without a valid sample are left out. In 0.31 they were reported as `0`, which looked like a counter reset. - An explicit instance list such as `instances: ["C:", "D:"]` now returns one series per instance. In 0.31 all listed instances were merged into one series. - When Windows reports the same instance name more than once, for example every `svchost` process of the `Process` object, the `performancecounter`, `netframework`, `hyperv` and `exchange` (ADAccess processes) collectors publish each occurrence as a separate series. The first keeps the name, later ones get the suffix `#1`, `#2` and so on (`exchange` keeps its `_2`, `_3` label suffixes), which depends on the order of the sample and does not identify the instance across scrapes. The other PDH-based collectors read objects that Windows names uniquely; if one still reports a name twice, they publish the first instance with valid data and log a warning. In 0.31 the occurrences overwrote each other. diff --git a/internal/collector/adfs/AGENTS.md b/internal/collector/adfs/AGENTS.md index 90550bcf..d32f912f 100644 --- a/internal/collector/adfs/AGENTS.md +++ b/internal/collector/adfs/AGENTS.md @@ -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. diff --git a/internal/collector/adfs/adfs.go b/internal/collector/adfs/adfs.go index 6e3ebcb1..05e53308 100644 --- a/internal/collector/adfs/adfs.go +++ b/internal/collector/adfs/adfs.go @@ -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, diff --git a/internal/collector/adfs/adfs_internal_test.go b/internal/collector/adfs/adfs_internal_test.go index 047d9c5b..1afdbb9d 100644 --- a/internal/collector/adfs/adfs_internal_test.go +++ b/internal/collector/adfs/adfs_internal_test.go @@ -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) } diff --git a/internal/collector/adfs/types.go b/internal/collector/adfs/types.go index 0f840319..fc5dd216 100644 --- a/internal/collector/adfs/types.go +++ b/internal/collector/adfs/types.go @@ -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"` diff --git a/internal/collector/container/container.go b/internal/collector/container/container.go index baf5829f..a0e151ed 100644 --- a/internal/collector/container/container.go +++ b/internal/collector/container/container.go @@ -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, ) diff --git a/internal/collector/cpu/AGENTS.md b/internal/collector/cpu/AGENTS.md index b48fb208..298f9114 100644 --- a/internal/collector/cpu/AGENTS.md +++ b/internal/collector/cpu/AGENTS.md @@ -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 diff --git a/internal/collector/gpu/gpu.go b/internal/collector/gpu/gpu.go index 7bb31507..c33003b9 100644 --- a/internal/collector/gpu/gpu.go +++ b/internal/collector/gpu/gpu.go @@ -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, ) } diff --git a/internal/collector/hyperv/hyperv_virtual_storage_device.go b/internal/collector/hyperv/hyperv_virtual_storage_device.go index 8ddba046..4f7c6228 100644 --- a/internal/collector/hyperv/hyperv_virtual_storage_device.go +++ b/internal/collector/hyperv/hyperv_virtual_storage_device.go @@ -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, ) } diff --git a/internal/collector/logical_disk/AGENTS.md b/internal/collector/logical_disk/AGENTS.md index a983e322..3d319e32 100644 --- a/internal/collector/logical_disk/AGENTS.md +++ b/internal/collector/logical_disk/AGENTS.md @@ -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 diff --git a/internal/collector/physical_disk/AGENTS.md b/internal/collector/physical_disk/AGENTS.md index 02aa17fd..d75e9680 100644 --- a/internal/collector/physical_disk/AGENTS.md +++ b/internal/collector/physical_disk/AGENTS.md @@ -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 diff --git a/internal/collector/physical_disk/physical_disk.go b/internal/collector/physical_disk/physical_disk.go index fec57aaa..6d587674 100644 --- a/internal/collector/physical_disk/physical_disk.go +++ b/internal/collector/physical_disk/physical_disk.go @@ -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, ) } diff --git a/internal/collector/process/process.go b/internal/collector/process/process.go index 6ed734b9..0e5efdb6 100644 --- a/internal/collector/process/process.go +++ b/internal/collector/process/process.go @@ -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", ) diff --git a/internal/collector/smbclient/AGENTS.md b/internal/collector/smbclient/AGENTS.md index df9c4e26..fc4cae2b 100644 --- a/internal/collector/smbclient/AGENTS.md +++ b/internal/collector/smbclient/AGENTS.md @@ -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`). diff --git a/internal/collector/smbclient/smbclient.go b/internal/collector/smbclient/smbclient.go index 46a90214..ef4470ce 100644 --- a/internal/collector/smbclient/smbclient.go +++ b/internal/collector/smbclient/smbclient.go @@ -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 diff --git a/internal/collector/smbclient/smbclient_internal_test.go b/internal/collector/smbclient/smbclient_internal_test.go index 1c1dc076..ab278931 100644 --- a/internal/collector/smbclient/smbclient_internal_test.go +++ b/internal/collector/smbclient/smbclient_internal_test.go @@ -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 diff --git a/internal/headers/kernel32/kernel32.go b/internal/headers/kernel32/kernel32.go index 4f77b18f..f1c4d3b2 100644 --- a/internal/headers/kernel32/kernel32.go +++ b/internal/headers/kernel32/kernel32.go @@ -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 +} diff --git a/internal/pdh/collector.go b/internal/pdh/collector.go index ea9c56a6..5328110b 100644 --- a/internal/pdh/collector.go +++ b/internal/pdh/collector.go @@ -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 { diff --git a/internal/pdh/collector_internal_test.go b/internal/pdh/collector_internal_test.go index 8c20f08e..d9397ee6 100644 --- a/internal/pdh/collector_internal_test.go +++ b/internal/pdh/collector_internal_test.go @@ -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)) } diff --git a/internal/pdh/const.go b/internal/pdh/const.go index 36b24268..23fad5c7 100644 --- a/internal/pdh/const.go +++ b/internal/pdh/const.go @@ -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, }