diff --git a/internal/pdh/collector.go b/internal/pdh/collector.go index 139a5437..371555c5 100644 --- a/internal/pdh/collector.go +++ b/internal/pdh/collector.go @@ -509,6 +509,8 @@ func (c *Collector[T]) collect(dst *[]T, state *collectState) (err error) { defer state.names.removeUnseen() for counterIndex := range c.counters { + c.loadTimeBase(&c.counters[counterIndex]) + for _, instance := range c.counters[counterIndex].Instances { itemCount, ok, err := c.getCounterArray(instance, &state.buf) if err != nil { @@ -538,6 +540,26 @@ func (c *Collector[T]) collect(dst *[]T, state *collectState) (err error) { return nil } +// loadTimeBase reads the time base of a raw elapsed time counter that had none +// when 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, the counter has no valid value and every instance is omitted. +func (c *Collector[T]) loadTimeBase(counter *Counter) { + if c.resultType != CounterTypeRaw || counter.Type != PERF_ELAPSED_TIME || counter.Frequency > 0 { + return + } + + for _, instance := range counter.Instances { + var frequency int64 + + if ret := GetCounterTimeBase(instance, &frequency); ret == ErrorSuccess && frequency > 0 { + counter.Frequency = frequency + + return + } + } +} + // pdhBufferSlack is extra space allocated beyond the buffer size reported by PDH. // On Windows Server 2012 R2, PdhGetRawCounterArrayW reports the size of the item array of a single-instance // object (e.g. System) without the terminator of the empty instance name, and writes that terminator right after diff --git a/internal/pdh/collector_internal_test.go b/internal/pdh/collector_internal_test.go index e4150e3d..e9395ac5 100644 --- a/internal/pdh/collector_internal_test.go +++ b/internal/pdh/collector_internal_test.go @@ -363,6 +363,40 @@ func TestSetRawValueElapsedTime(t *testing.T) { require.True(t, c.setRawValue(&values, counter, RawCounter{FirstValue: 1, SecondValue: 2})) } +// 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). +func TestCollectLoadsMissingTimeBase(t *testing.T) { + t.Parallel() + + type systemUpTime struct { + SystemUpTime float64 `perfdata:"System Up Time"` + } + + collector, err := NewCollector[systemUpTime](slog.New(slog.DiscardHandler), CounterTypeRaw, "System", nil) + require.NoError(t, err) + + t.Cleanup(collector.Close) + + require.Len(t, collector.counters, 1) + + counter := &collector.counters[0] + require.Equal(t, uint32(PERF_ELAPSED_TIME), counter.Type) + + frequency := counter.Frequency + require.Positive(t, frequency) + + // As if GetCounterTimeBase reported no data at startup. + counter.Frequency = 0 + + var dst []systemUpTime + + require.NoError(t, collector.Collect(&dst)) + require.Equal(t, frequency, counter.Frequency) + require.Len(t, dst, 1) + require.Positive(t, dst[0].SystemUpTime) +} + func TestMetricTypeForCounterType(t *testing.T) { t.Parallel()