mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-19 02:59:04 +02:00
process: read processes from the kernel instead of perflib (#2644)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4ca7dc8957
commit
651daf0fee
@@ -26,7 +26,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/pdh"
|
||||
"github.com/prometheus-community/windows_exporter/internal/pdh/registry"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -78,30 +77,22 @@ func TestTotalSuffixProcess(t *testing.T) {
|
||||
ID float64 `perfdata:"ID Process"`
|
||||
}
|
||||
|
||||
legacy, err := registry.NewCollector[processValues]("Process", pdh.InstancesAll)
|
||||
collector, err := pdh.NewCollector[processValues](slog.New(slog.DiscardHandler), pdh.CounterTypeRaw, "Process", []string{"worker_Total"})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(legacy.Close)
|
||||
t.Cleanup(collector.Close)
|
||||
|
||||
modern, err := pdh.NewCollector[processValues](slog.New(slog.DiscardHandler), pdh.CounterTypeRaw, "Process", []string{"worker_Total"})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(modern.Close)
|
||||
require.Eventually(t, func() bool {
|
||||
var rows []processValues
|
||||
if collector.Collect(&rows) != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for name, collect := range map[string]func(*[]processValues) error{"registry": legacy.Collect, "explicit PDH": modern.Collect} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
require.Eventually(t, func() bool {
|
||||
var rows []processValues
|
||||
if collect(&rows) != nil {
|
||||
return false
|
||||
}
|
||||
for _, row := range rows {
|
||||
if row.ID == float64(child.Process.Pid) && row.Name == "worker_Total" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
if row.ID == float64(child.Process.Pid) && row.Name == "worker_Total" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}, time.Second*5, time.Millisecond*100)
|
||||
})
|
||||
}
|
||||
return false
|
||||
}, time.Second*5, time.Millisecond*100)
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Leopold Schabel / The perflib_exporter authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,229 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/pdh"
|
||||
)
|
||||
|
||||
type Collector[T any] struct {
|
||||
object string
|
||||
query string
|
||||
|
||||
counters map[string]Counter
|
||||
nameIndexValue int
|
||||
|
||||
// bufferMu guards buffer, the raw data buffer of the last query. A Collect
|
||||
// takes it out for the duration of the query, so concurrent calls never
|
||||
// share it; they allocate their own buffer instead.
|
||||
bufferMu sync.Mutex
|
||||
buffer []byte
|
||||
}
|
||||
|
||||
type Counter struct {
|
||||
Name string
|
||||
Desc string
|
||||
Instances map[string]uint32
|
||||
Type uint32
|
||||
Frequency float64
|
||||
|
||||
FieldIndexValue int
|
||||
FieldIndexSecondValue int
|
||||
}
|
||||
|
||||
// NewCollector creates a collector for the counters declared by the perfdata_v1 or perfdata tags of the struct T.
|
||||
func NewCollector[T any](object string, _ []string) (*Collector[T], error) {
|
||||
valueType := reflect.TypeFor[T]()
|
||||
if valueType.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("expected a struct, got %s", valueType)
|
||||
}
|
||||
|
||||
if err := CounterNameTable.load(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := MapCounterToIndex(object)
|
||||
if query == "0" {
|
||||
return nil, fmt.Errorf("perflib object %q not found in the counter name table", object)
|
||||
}
|
||||
|
||||
collector := &Collector[T]{
|
||||
object: object,
|
||||
query: query,
|
||||
nameIndexValue: -1,
|
||||
counters: make(map[string]Counter),
|
||||
}
|
||||
|
||||
if f, ok := valueType.FieldByName("Name"); ok {
|
||||
if f.Type.Kind() == reflect.String {
|
||||
collector.nameIndexValue = f.Index[0]
|
||||
}
|
||||
}
|
||||
|
||||
for _, f := range reflect.VisibleFields(valueType) {
|
||||
tag, ok := f.Tag.Lookup("perfdata_v1")
|
||||
if !ok {
|
||||
tag, ok = f.Tag.Lookup("perfdata")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if f.Type.Kind() != reflect.Float64 {
|
||||
return nil, fmt.Errorf("field %s must be a float64", f.Name)
|
||||
}
|
||||
|
||||
counterName, secondValue := strings.CutSuffix(tag, ",secondvalue")
|
||||
|
||||
counter, ok := collector.counters[counterName]
|
||||
if !ok {
|
||||
counter = Counter{
|
||||
Name: counterName,
|
||||
FieldIndexSecondValue: -1,
|
||||
FieldIndexValue: -1,
|
||||
}
|
||||
}
|
||||
|
||||
if secondValue {
|
||||
counter.FieldIndexSecondValue = f.Index[0]
|
||||
} else {
|
||||
counter.FieldIndexValue = f.Index[0]
|
||||
}
|
||||
|
||||
collector.counters[counterName] = counter
|
||||
}
|
||||
|
||||
var collectValues []T
|
||||
|
||||
if err := collector.Collect(&collectValues); err != nil {
|
||||
return nil, fmt.Errorf("failed to collect initial data: %w", err)
|
||||
}
|
||||
|
||||
return collector, nil
|
||||
}
|
||||
|
||||
func (c *Collector[T]) Describe() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
// Collect replaces the content of dst with one row per collected instance.
|
||||
func (c *Collector[T]) Collect(dst *[]T) error {
|
||||
if dst == nil {
|
||||
return errors.New("dst must not be nil")
|
||||
}
|
||||
|
||||
c.bufferMu.Lock()
|
||||
buffer := c.buffer
|
||||
c.buffer = nil
|
||||
c.bufferMu.Unlock()
|
||||
|
||||
perfObjects, buffer, err := queryPerformanceData(c.query, c.object, buffer)
|
||||
|
||||
// Keep the larger buffer if a concurrent call put one back in the meantime.
|
||||
c.bufferMu.Lock()
|
||||
if cap(buffer) > cap(c.buffer) {
|
||||
c.buffer = buffer
|
||||
}
|
||||
c.bufferMu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("QueryPerformanceData: %w", err)
|
||||
}
|
||||
|
||||
if len(perfObjects) == 0 || perfObjects[0] == nil || len(perfObjects[0].Instances) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
*dst = make([]T, 0, len(perfObjects[0].Instances))
|
||||
|
||||
for _, perfObject := range perfObjects {
|
||||
if perfObject.Name != c.object {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, perfInstance := range perfObject.Instances {
|
||||
instanceName := perfInstance.Name
|
||||
if pdh.IsTotalInstance(c.object, instanceName) {
|
||||
continue
|
||||
}
|
||||
|
||||
if instanceName == "" || instanceName == "*" {
|
||||
instanceName = pdh.InstanceEmpty
|
||||
}
|
||||
|
||||
var row T
|
||||
|
||||
rv := reflect.ValueOf(&row).Elem()
|
||||
|
||||
if c.nameIndexValue != -1 {
|
||||
rv.Field(c.nameIndexValue).SetString(instanceName)
|
||||
}
|
||||
|
||||
for _, perfCounter := range perfInstance.Counters {
|
||||
if perfCounter.Def.IsBaseValue && !perfCounter.Def.IsNanosecondCounter {
|
||||
continue
|
||||
}
|
||||
|
||||
counter, ok := c.counters[perfCounter.Def.Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch perfCounter.Def.CounterType {
|
||||
case pdh.PERF_ELAPSED_TIME:
|
||||
// A zero frequency would divide by zero. The value is left unset.
|
||||
if perfObject.Frequency <= 0 || counter.FieldIndexValue == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
rv.Field(counter.FieldIndexValue).
|
||||
SetFloat(float64((perfCounter.Value - pdh.WindowsEpoch) / perfObject.Frequency))
|
||||
case pdh.PERF_100NSEC_TIMER, pdh.PERF_PRECISION_100NS_TIMER:
|
||||
if counter.FieldIndexValue == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
rv.Field(counter.FieldIndexValue).
|
||||
SetFloat(float64(perfCounter.Value) * pdh.TicksToSecondScaleFactor)
|
||||
default:
|
||||
if counter.FieldIndexSecondValue != -1 {
|
||||
rv.Field(counter.FieldIndexSecondValue).
|
||||
SetFloat(float64(perfCounter.SecondValue))
|
||||
}
|
||||
|
||||
if counter.FieldIndexValue != -1 {
|
||||
rv.Field(counter.FieldIndexValue).
|
||||
SetFloat(float64(perfCounter.Value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*dst = append(*dst, row)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Collector[T]) Close() {}
|
||||
@@ -1,125 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// CounterNameTable Initialize global name tables
|
||||
// profiling, add option to disable name tables if necessary
|
||||
// Not sure if we should resolve the names at all or just have the caller do it on demand
|
||||
// (for many use cases the index is sufficient)
|
||||
//
|
||||
//nolint:gochecknoglobals
|
||||
var CounterNameTable = QueryNameTable("Counter 009")
|
||||
|
||||
func (p *perfObjectType) LookupName() string {
|
||||
return CounterNameTable.LookupString(p.ObjectNameTitleIndex)
|
||||
}
|
||||
|
||||
type NameTable struct {
|
||||
mu sync.Mutex
|
||||
|
||||
name string
|
||||
|
||||
table struct {
|
||||
index map[uint32]string
|
||||
string map[string]uint32
|
||||
}
|
||||
}
|
||||
|
||||
// LookupString returns the name for index. It returns an empty string if the
|
||||
// index is unknown or the name table could not be loaded.
|
||||
func (t *NameTable) LookupString(index uint32) string {
|
||||
if t.load() != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return t.table.index[index]
|
||||
}
|
||||
|
||||
// LookupIndex returns the index for str. It returns 0 if the name is unknown
|
||||
// or the name table could not be loaded.
|
||||
func (t *NameTable) LookupIndex(str string) uint32 {
|
||||
if t.load() != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return t.table.string[str]
|
||||
}
|
||||
|
||||
// QueryNameTable Query a perflib name table from the v1. Specify the type and the language
|
||||
// code (i.e. "Counter 009" or "Help 009") for English language.
|
||||
func QueryNameTable(tableName string) *NameTable {
|
||||
return &NameTable{
|
||||
name: tableName,
|
||||
}
|
||||
}
|
||||
|
||||
// load reads the name table from the registry. A failed load is retried on the
|
||||
// next call, so a transient error does not disable the table for good. The
|
||||
// maps are not modified once loaded.
|
||||
func (t *NameTable) load() error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.table.index != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
buffer, err := queryRawData(t.name, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query perflib name table %q: %w", t.name, err)
|
||||
}
|
||||
|
||||
t.table.index, t.table.string = parseNameTable(buffer)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseNameTable parses a name table, a REG_MULTI_SZ of alternating index and
|
||||
// name strings. Parsing stops at the first incomplete pair.
|
||||
func parseNameTable(buffer []byte) (map[uint32]string, map[string]uint32) {
|
||||
index := make(map[uint32]string)
|
||||
names := make(map[string]uint32)
|
||||
|
||||
r := bytes.NewReader(buffer)
|
||||
|
||||
for {
|
||||
indexStr, err := readUTF16String(r)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
desc, err := readUTF16String(r)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
indexInt, _ := strconv.ParseUint(indexStr, 10, 32)
|
||||
|
||||
index[uint32(indexInt)] = desc
|
||||
names[desc] = uint32(indexInt)
|
||||
}
|
||||
|
||||
return index, names
|
||||
}
|
||||
@@ -1,594 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package registry
|
||||
|
||||
/*
|
||||
Go bindings for the HKEY_PERFORMANCE_DATA perflib / Performance Counters interface.
|
||||
|
||||
# Overview
|
||||
|
||||
HKEY_PERFORMANCE_DATA is a low-level alternative to the higher-level PDH library and WMI.
|
||||
It operates on blocks of counters and only returns raw values without calculating rates
|
||||
or formatting them, which is exactly what you want for, say, a Prometheus exporter
|
||||
(not so much for a GUI like Windows Performance Monitor).
|
||||
|
||||
Its overhead is much lower than the high-level libraries.
|
||||
|
||||
It operates on the same set of perflib providers as PDH and WMI. See this document
|
||||
for more details on the relationship between the different libraries:
|
||||
https://msdn.microsoft.com/en-us/library/windows/desktop/aa371643(v=vs.85).aspx
|
||||
|
||||
Example C++ source code:
|
||||
https://msdn.microsoft.com/de-de/library/windows/desktop/aa372138(v=vs.85).aspx
|
||||
|
||||
For now, the API is not stable and is probably going to change in future
|
||||
perflib_exporter releases. If you want to use this library, send the author an email
|
||||
so we can discuss your requirements and stabilize the API.
|
||||
|
||||
# Names
|
||||
|
||||
Counter names and help texts are resolved by looking up an index in a name table.
|
||||
Since Microsoft loves internalization, both names and help texts can be requested
|
||||
any locally available language.
|
||||
|
||||
The library automatically loads the name tables and resolves all identifiers
|
||||
in English ("Name" and "HelpText" struct members). You can manually resolve
|
||||
identifiers in a different language by using the NameTable API.
|
||||
|
||||
# Performance Counters intro
|
||||
|
||||
Windows has a system-wide performance counter mechanism. Most performance counters
|
||||
are stored as actual counters, not gauges (with some exceptions).
|
||||
There's additional metadata which defines how the counter should be presented to the user
|
||||
(for example, as a calculated rate). This library disregards all of the display metadata.
|
||||
|
||||
At the top level, there's a number of performance counter objects.
|
||||
Each object has counter definitions, which contain the metadata for a particular
|
||||
counter, and either zero or multiple instances. We hide the fact that there are
|
||||
objects with no instances, and simply return a single null instance.
|
||||
|
||||
There's one counter per counter definition and instance (or the object itself, if
|
||||
there are no instances).
|
||||
|
||||
Behind the scenes, every perflib DLL provides one or more objects.
|
||||
Perflib has a v1 where DLLs are dynamically registered and
|
||||
unregistered. Some third party applications like VMWare provide their own counters,
|
||||
but this is, sadly, a rare occurrence.
|
||||
|
||||
Different Windows releases have different numbers of counters.
|
||||
|
||||
Objects and counters are identified by well-known indices.
|
||||
|
||||
Here's an example object with one instance:
|
||||
|
||||
4320 WSMan Quota Statistics [7 counters, 1 instance(s)]
|
||||
`-- "WinRMService"
|
||||
`-- Total Requests/Second [4322] = 59
|
||||
`-- User Quota Violations/Second [4324] = 0
|
||||
`-- System Quota Violations/Second [4326] = 0
|
||||
`-- Active Shells [4328] = 0
|
||||
`-- Active Operations [4330] = 0
|
||||
`-- Active Users [4332] = 0
|
||||
`-- Process ID [4334] = 928
|
||||
|
||||
All "per second" metrics are counters, the rest are gauges.
|
||||
|
||||
Another example, with no instance:
|
||||
|
||||
4600 Network QoS Policy [6 counters, 1 instance(s)]
|
||||
`-- (default)
|
||||
`-- Packets transmitted [4602] = 1744
|
||||
`-- Packets transmitted/sec [4604] = 4852
|
||||
`-- Bytes transmitted [4606] = 4853
|
||||
`-- Bytes transmitted/sec [4608] = 180388626632
|
||||
`-- Packets dropped [4610] = 0
|
||||
`-- Packets dropped/sec [4612] = 0
|
||||
|
||||
You can access the same values using PowerShell's Get-Counter cmdlet
|
||||
or the Performance Monitor.
|
||||
|
||||
> Get-Counter '\WSMan Quota Statistics(WinRMService)\Process ID'
|
||||
|
||||
Timestamp CounterSamples
|
||||
--------- --------------
|
||||
1/28/2018 10:18:00 PM \\DEV\wsman quota statistics(winrmservice)\process id :
|
||||
928
|
||||
|
||||
> (Get-Counter '\Process(Idle)\% Processor Time').CounterSamples[0] | Format-List *
|
||||
[..detailed output...]
|
||||
|
||||
Data for some of the objects is also available through WMI:
|
||||
|
||||
> Get-CimInstance Win32_PerfRawData_Counters_WSManQuotaStatistics
|
||||
|
||||
Name : WinRMService
|
||||
[...]
|
||||
ActiveOperations : 0
|
||||
ActiveShells : 0
|
||||
ActiveUsers : 0
|
||||
ProcessID : 928
|
||||
SystemQuotaViolationsPerSecond : 0
|
||||
TotalRequestsPerSecond : 59
|
||||
UserQuotaViolationsPerSecond : 0
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/pdh"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// There's a LittleEndian field in the PERF header - we ought to check it.
|
||||
//
|
||||
//nolint:gochecknoglobals
|
||||
var bo = binary.LittleEndian
|
||||
|
||||
// PerfObject Top-level performance object (like "Process").
|
||||
type PerfObject struct {
|
||||
Name string
|
||||
// NameIndex Same index you pass to QueryPerformanceData
|
||||
NameIndex uint
|
||||
Instances []PerfInstance
|
||||
CounterDefs []PerfCounterDef
|
||||
|
||||
Frequency int64
|
||||
}
|
||||
|
||||
// PerfInstance Each object can have multiple instances. For example,
|
||||
// In case the object has no instances, we return one single PerfInstance with an empty name.
|
||||
type PerfInstance struct {
|
||||
// *not* resolved using a name table
|
||||
Name string
|
||||
// Counters holds one counter per counter definition of the object, in the
|
||||
// same order. The counters of all instances share one backing array.
|
||||
Counters []PerfCounter
|
||||
}
|
||||
|
||||
type PerfCounterDef struct {
|
||||
Name string
|
||||
NameIndex uint
|
||||
|
||||
// For debugging - subject to removal. CounterType is a perflib
|
||||
// implementation detail (see perflib.h) and should not be used outside
|
||||
// of this package. We export it so we can show it on /dump.
|
||||
CounterType uint32
|
||||
|
||||
// PERF_TYPE_COUNTER (otherwise, it's a gauge)
|
||||
IsCounter bool
|
||||
// PERF_COUNTER_BASE (base value of a multi-value fraction)
|
||||
IsBaseValue bool
|
||||
// PERF_TIMER_100NS
|
||||
IsNanosecondCounter bool
|
||||
HasSecondValue bool
|
||||
|
||||
rawData perfCounterDefinition
|
||||
}
|
||||
|
||||
type PerfCounter struct {
|
||||
Value int64
|
||||
Def *PerfCounterDef
|
||||
SecondValue int64
|
||||
}
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
bufLenGlobal = uint32(400000)
|
||||
bufLenCostly = uint32(2000000)
|
||||
)
|
||||
|
||||
// queryRawData Queries the performance counter buffer using RegQueryValueEx, returning raw bytes. See:
|
||||
// https://msdn.microsoft.com/de-de/library/windows/desktop/aa373219(v=vs.85).aspx
|
||||
//
|
||||
// If buffer has enough capacity for the initial size guess, it is reused instead
|
||||
// of allocating a new one. The returned slice may share memory with buffer.
|
||||
func queryRawData(query string, buffer []byte) ([]byte, error) {
|
||||
var (
|
||||
valType uint32
|
||||
bufLen uint32
|
||||
)
|
||||
|
||||
switch query {
|
||||
case "Global":
|
||||
bufLen = bufLenGlobal
|
||||
case "Costly":
|
||||
bufLen = bufLenCostly
|
||||
default:
|
||||
// depends on the number of values requested
|
||||
// need make an educated guess
|
||||
numCounters := len(strings.Split(query, " "))
|
||||
bufLen = uint32(150000 * numCounters)
|
||||
}
|
||||
|
||||
// A buffer from a previous call keeps any growth from ERROR_MORE_DATA, so
|
||||
// using its full capacity avoids growing it again.
|
||||
if uint32(cap(buffer)) >= bufLen {
|
||||
buffer = buffer[:cap(buffer)]
|
||||
} else {
|
||||
buffer = make([]byte, bufLen)
|
||||
}
|
||||
|
||||
name, err := windows.UTF16PtrFromString(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode query string: %w", err)
|
||||
}
|
||||
|
||||
for {
|
||||
bufLen := uint32(len(buffer))
|
||||
|
||||
err := windows.RegQueryValueEx(
|
||||
windows.HKEY_PERFORMANCE_DATA,
|
||||
name,
|
||||
nil,
|
||||
&valType,
|
||||
(*byte)(unsafe.Pointer(&buffer[0])),
|
||||
&bufLen)
|
||||
|
||||
switch {
|
||||
case errors.Is(err, error(windows.ERROR_MORE_DATA)):
|
||||
// Exponential buffer growth prevents O(N) allocation spin-loops under heavy load.
|
||||
// The previous copy() was removed because the buffer contents are
|
||||
// incomplete/invalid and will be overwritten on the next API call.
|
||||
buffer = make([]byte, len(buffer)*2)
|
||||
|
||||
continue
|
||||
case errors.Is(err, error(windows.ERROR_BUSY)):
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
continue
|
||||
case err != nil:
|
||||
if errNo, ok := errors.AsType[windows.Errno](err); ok {
|
||||
return nil, fmt.Errorf("ReqQueryValueEx failed: %w errno %d", err, uint(errNo))
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buffer = buffer[:bufLen]
|
||||
|
||||
switch query {
|
||||
case "Global":
|
||||
if bufLen > bufLenGlobal {
|
||||
bufLenGlobal = bufLen
|
||||
}
|
||||
case "Costly":
|
||||
if bufLen > bufLenCostly {
|
||||
bufLenCostly = bufLen
|
||||
}
|
||||
}
|
||||
|
||||
return buffer, nil
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
QueryPerformanceData Query all performance counters that match a given query.
|
||||
|
||||
The query can be any of the following:
|
||||
|
||||
- "Global" (all performance counters except those Windows marked as costly)
|
||||
|
||||
- "Costly" (only the costly ones)
|
||||
|
||||
- One or more object indices, separated by spaces ("238 2 5")
|
||||
|
||||
Many objects have dependencies - if you query one of them, you often get back
|
||||
more than you asked for.
|
||||
*/
|
||||
func QueryPerformanceData(query string, counterName string) ([]*PerfObject, error) {
|
||||
objects, _, err := queryPerformanceData(query, counterName, nil)
|
||||
|
||||
return objects, err
|
||||
}
|
||||
|
||||
// queryPerformanceData is QueryPerformanceData with a reusable raw data buffer.
|
||||
// It returns the buffer for the next call. The parsed objects copy everything
|
||||
// they need out of the buffer, so the buffer can be reused once this returns.
|
||||
func queryPerformanceData(query string, counterName string, buffer []byte) ([]*PerfObject, []byte, error) {
|
||||
// Object and counter names are resolved through the name table.
|
||||
if err := CounterNameTable.load(); err != nil {
|
||||
return nil, buffer, err
|
||||
}
|
||||
|
||||
buffer, err := queryRawData(query, buffer)
|
||||
if err != nil {
|
||||
return nil, buffer, err
|
||||
}
|
||||
|
||||
objects, err := parsePerformanceData(buffer, counterName)
|
||||
if err != nil {
|
||||
return nil, buffer, fmt.Errorf("failed to parse performance data for %q: %w", query, err)
|
||||
}
|
||||
|
||||
return objects, buffer, nil
|
||||
}
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
perfDataBlockSize = int64(binary.Size(perfDataBlock{}))
|
||||
perfObjectTypeSize = int64(binary.Size(perfObjectType{}))
|
||||
perfCounterDefinitionSize = int64(binary.Size(perfCounterDefinition{}))
|
||||
perfInstanceDefinitionSize = int64(binary.Size(perfInstanceDefinition{}))
|
||||
perfCounterBlockSize = int64(binary.Size(perfCounterBlock{}))
|
||||
)
|
||||
|
||||
// errMalformedPerformanceData is returned when the performance data contradicts
|
||||
// itself, e.g. it declares more objects than fit into the buffer. Counts,
|
||||
// offsets and lengths are checked before they size an allocation or index into
|
||||
// the buffer.
|
||||
var errMalformedPerformanceData = errors.New("malformed performance data")
|
||||
|
||||
// parsePerformanceData parses a PERF_DATA_BLOCK as returned by queryRawData.
|
||||
// If counterName is set, only the first object with that name is returned.
|
||||
//
|
||||
// The structures are decoded straight from buffer. The parsed objects copy
|
||||
// everything they keep, so they do not reference buffer.
|
||||
func parsePerformanceData(buffer []byte, counterName string) ([]*PerfObject, error) {
|
||||
bufLen := int64(len(buffer))
|
||||
|
||||
// Read global header
|
||||
|
||||
var header perfDataBlock
|
||||
|
||||
if err := header.decode(buffer); err != nil {
|
||||
return nil, fmt.Errorf("failed to read performance data block: %w", err)
|
||||
}
|
||||
|
||||
// Check for "PERF" signature
|
||||
if header.Signature != [4]uint16{80, 69, 82, 70} {
|
||||
return nil, fmt.Errorf("%w: invalid performance block signature %v", errMalformedPerformanceData, header.Signature)
|
||||
}
|
||||
|
||||
// Parse the performance data
|
||||
|
||||
numObjects := int64(header.NumObjectTypes)
|
||||
objOffset := int64(header.HeaderLength)
|
||||
|
||||
if objOffset < perfDataBlockSize || numObjects*perfObjectTypeSize > bufLen-objOffset {
|
||||
return nil, fmt.Errorf("%w: %d objects at offset %d do not fit into %d bytes", errMalformedPerformanceData, numObjects, objOffset, bufLen)
|
||||
}
|
||||
|
||||
numFilteredObjects := 0
|
||||
|
||||
objects := make([]*PerfObject, numObjects)
|
||||
|
||||
// nameScratch holds the UTF16 code units of an instance name while it is decoded.
|
||||
var nameScratch []uint16
|
||||
|
||||
for i := range numObjects {
|
||||
var obj perfObjectType
|
||||
|
||||
if err := obj.decode(bytesAt(buffer, objOffset)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
perfCounterName := obj.LookupName()
|
||||
|
||||
if counterName != "" && perfCounterName != counterName {
|
||||
objOffset += int64(obj.TotalByteLength)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
numCounterDefs := int64(obj.NumCounters)
|
||||
numInstances := int64(obj.NumInstances)
|
||||
|
||||
if numCounterDefs*perfCounterDefinitionSize > bufLen-objOffset-perfObjectTypeSize {
|
||||
return nil, fmt.Errorf("%w: %d counter definitions do not fit into the buffer", errMalformedPerformanceData, numCounterDefs)
|
||||
}
|
||||
|
||||
// Every instance has at least an instance definition and a counter block.
|
||||
if numInstances*(perfInstanceDefinitionSize+perfCounterBlockSize) > bufLen-objOffset {
|
||||
return nil, fmt.Errorf("%w: %d instances do not fit into the buffer", errMalformedPerformanceData, numInstances)
|
||||
}
|
||||
|
||||
// Perf objects can have no instances. The perflib differentiates
|
||||
// between objects with instances and without, but we just create
|
||||
// an empty instance in order to simplify the interface.
|
||||
if numInstances <= 0 {
|
||||
numInstances = 1
|
||||
}
|
||||
|
||||
// The counters of all instances share one backing array below, which
|
||||
// grows with instances × counters. Counter values do not overlap, so
|
||||
// each takes at least one byte of the object. Checking that keeps the
|
||||
// allocation linear in the buffer size for malformed data.
|
||||
if numInstances*numCounterDefs > bufLen-objOffset {
|
||||
return nil, fmt.Errorf("%w: %d instances with %d counters each do not fit into the buffer", errMalformedPerformanceData, numInstances, numCounterDefs)
|
||||
}
|
||||
|
||||
instances := make([]PerfInstance, numInstances)
|
||||
counterDefs := make([]PerfCounterDef, numCounterDefs)
|
||||
counters := make([]PerfCounter, numInstances*numCounterDefs)
|
||||
|
||||
objects[i] = &PerfObject{
|
||||
Name: perfCounterName,
|
||||
NameIndex: uint(obj.ObjectNameTitleIndex),
|
||||
Instances: instances,
|
||||
CounterDefs: counterDefs,
|
||||
Frequency: obj.PerfFreq,
|
||||
}
|
||||
|
||||
// The counter definitions follow the object header.
|
||||
defOffset := objOffset + perfObjectTypeSize
|
||||
|
||||
for j := range numCounterDefs {
|
||||
var def perfCounterDefinition
|
||||
|
||||
if err := def.decode(bytesAt(buffer, defOffset+j*perfCounterDefinitionSize)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
counterDefs[j] = PerfCounterDef{
|
||||
Name: def.LookupName(),
|
||||
NameIndex: uint(def.CounterNameTitleIndex),
|
||||
rawData: def,
|
||||
|
||||
CounterType: def.CounterType,
|
||||
|
||||
IsCounter: def.CounterType&0x400 == 0x400,
|
||||
IsBaseValue: def.CounterType&0x00030000 == 0x00030000,
|
||||
IsNanosecondCounter: def.CounterType&0x00100000 == 0x00100000,
|
||||
HasSecondValue: def.CounterType == pdh.PERF_AVERAGE_BULK,
|
||||
}
|
||||
}
|
||||
|
||||
if obj.NumInstances <= 0 { //nolint:nestif
|
||||
blockOffset := objOffset + int64(obj.DefinitionLength)
|
||||
|
||||
if _, err := parseCounterBlock(buffer, blockOffset, counterDefs, counters); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instances[0].Counters = counters
|
||||
} else {
|
||||
instOffset := objOffset + int64(obj.DefinitionLength)
|
||||
|
||||
for j := range numInstances {
|
||||
var inst perfInstanceDefinition
|
||||
|
||||
if err := inst.decode(bytesAt(buffer, instOffset)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
namePos := instOffset + int64(inst.NameOffset)
|
||||
if namePos+int64(inst.NameLength) > bufLen {
|
||||
return nil, fmt.Errorf("%w: instance name at offset %d with length %d exceeds the buffer", errMalformedPerformanceData, namePos, inst.NameLength)
|
||||
}
|
||||
|
||||
instances[j].Name, nameScratch = decodeUTF16String(buffer[namePos:namePos+int64(inst.NameLength)], nameScratch)
|
||||
|
||||
blockOffset := instOffset + int64(inst.ByteLength)
|
||||
instCounters := counters[j*numCounterDefs : (j+1)*numCounterDefs : (j+1)*numCounterDefs]
|
||||
|
||||
blockLength, err := parseCounterBlock(buffer, blockOffset, counterDefs, instCounters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instances[j].Counters = instCounters
|
||||
instOffset = blockOffset + blockLength
|
||||
}
|
||||
}
|
||||
|
||||
if counterName != "" {
|
||||
return objects[i : i+1], nil
|
||||
}
|
||||
|
||||
// Next perfObjectType
|
||||
objOffset += int64(obj.TotalByteLength)
|
||||
numFilteredObjects++
|
||||
}
|
||||
|
||||
return objects[:numFilteredObjects], nil
|
||||
}
|
||||
|
||||
// bytesAt returns b from offset pos on, or nil if pos lies outside of b.
|
||||
func bytesAt(b []byte, pos int64) []byte {
|
||||
if pos < 0 || pos > int64(len(b)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return b[pos:]
|
||||
}
|
||||
|
||||
// parseCounterBlock reads the PERF_COUNTER_BLOCK at pos into counters, which
|
||||
// has one element per definition in defs. It returns the length of the block.
|
||||
func parseCounterBlock(b []byte, pos int64, defs []PerfCounterDef, counters []PerfCounter) (int64, error) {
|
||||
var block perfCounterBlock
|
||||
|
||||
if err := block.decode(bytesAt(b, pos)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// The block length includes the length field itself. A shorter block would
|
||||
// make the next instance overlap this one.
|
||||
if int64(block.ByteLength) < perfCounterBlockSize {
|
||||
return 0, fmt.Errorf("%w: counter block at offset %d has length %d", errMalformedPerformanceData, pos, block.ByteLength)
|
||||
}
|
||||
|
||||
for i := range defs {
|
||||
def := &defs[i]
|
||||
|
||||
valueOffset := pos + int64(def.rawData.CounterOffset)
|
||||
size := counterValueSize(&def.rawData)
|
||||
|
||||
end := valueOffset + size
|
||||
if def.HasSecondValue {
|
||||
end = valueOffset + 8 + size
|
||||
}
|
||||
|
||||
if end > int64(len(b)) {
|
||||
return 0, fmt.Errorf("%w: value of counter %s at offset %d exceeds the buffer", errMalformedPerformanceData, def.Name, valueOffset)
|
||||
}
|
||||
|
||||
value := convertCounterValue(&def.rawData, b, valueOffset)
|
||||
secondValue := int64(0)
|
||||
|
||||
if def.HasSecondValue {
|
||||
secondValue = convertCounterValue(&def.rawData, b, valueOffset+8)
|
||||
}
|
||||
|
||||
counters[i] = PerfCounter{
|
||||
Value: value,
|
||||
Def: def,
|
||||
SecondValue: secondValue,
|
||||
}
|
||||
}
|
||||
|
||||
return int64(block.ByteLength), nil
|
||||
}
|
||||
|
||||
// counterValueSize returns the number of bytes convertCounterValue reads.
|
||||
func counterValueSize(counterDef *perfCounterDefinition) int64 {
|
||||
if counterDef.CounterSize == 8 {
|
||||
return 8
|
||||
}
|
||||
|
||||
return 4
|
||||
}
|
||||
|
||||
func convertCounterValue(counterDef *perfCounterDefinition, buffer []byte, valueOffset int64) int64 {
|
||||
/*
|
||||
We can safely ignore the type since we're not interested in anything except the raw value.
|
||||
We also ignore all of the other attributes (timestamp, presentation, multi counter values...)
|
||||
|
||||
See also: winperf.h.
|
||||
|
||||
Here's the most common value for CounterType:
|
||||
|
||||
65536 32bit counter
|
||||
65792 64bit counter
|
||||
272696320 32bit rate
|
||||
272696576 64bit rate
|
||||
|
||||
*/
|
||||
switch counterDef.CounterSize {
|
||||
case 4:
|
||||
return int64(bo.Uint32(buffer[valueOffset:(valueOffset + 4)]))
|
||||
case 8:
|
||||
return int64(bo.Uint64(buffer[valueOffset:(valueOffset + 8)]))
|
||||
default:
|
||||
return int64(bo.Uint32(buffer[valueOffset:(valueOffset + 4)]))
|
||||
}
|
||||
}
|
||||
@@ -1,425 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/pdh"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// syntheticPerfData describes a PERF_DATA_BLOCK with one object and one
|
||||
// 64-bit counter, either without instances or with a single instance.
|
||||
type syntheticPerfData struct {
|
||||
header perfDataBlock
|
||||
object perfObjectType
|
||||
def perfCounterDefinition
|
||||
instance perfInstanceDefinition
|
||||
instanceName []uint16
|
||||
block perfCounterBlock
|
||||
value uint64
|
||||
|
||||
withInstance bool
|
||||
}
|
||||
|
||||
func newSyntheticPerfData(withInstance bool) *syntheticPerfData {
|
||||
d := &syntheticPerfData{
|
||||
withInstance: withInstance,
|
||||
instanceName: windows.StringToUTF16("inst"),
|
||||
value: 42,
|
||||
}
|
||||
|
||||
d.header.Signature = [4]uint16{'P', 'E', 'R', 'F'}
|
||||
d.header.LittleEndian = 1
|
||||
d.header.HeaderLength = uint32(perfDataBlockSize)
|
||||
d.header.NumObjectTypes = 1
|
||||
|
||||
d.object.HeaderLength = uint32(perfObjectTypeSize)
|
||||
d.object.DefinitionLength = uint32(perfObjectTypeSize + perfCounterDefinitionSize)
|
||||
d.object.NumCounters = 1
|
||||
d.object.NumInstances = -1 // PERF_NO_INSTANCES
|
||||
d.object.PerfFreq = 10_000_000
|
||||
|
||||
d.def.ByteLength = uint32(perfCounterDefinitionSize)
|
||||
d.def.CounterType = pdh.PERF_COUNTER_LARGE_RAWCOUNT
|
||||
d.def.CounterSize = 8
|
||||
// The value follows the 4-byte block length and 4 bytes of padding.
|
||||
d.def.CounterOffset = 8
|
||||
|
||||
d.block.ByteLength = 16
|
||||
|
||||
if withInstance {
|
||||
nameLength := uint32(len(d.instanceName) * 2)
|
||||
|
||||
d.object.NumInstances = 1
|
||||
d.instance.NameOffset = uint32(perfInstanceDefinitionSize)
|
||||
d.instance.NameLength = nameLength
|
||||
// The name is padded to 8 bytes.
|
||||
d.instance.ByteLength = uint32(perfInstanceDefinitionSize) + (nameLength+7)&^7
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *syntheticPerfData) bytes(tb testing.TB) []byte {
|
||||
tb.Helper()
|
||||
|
||||
var body bytes.Buffer
|
||||
|
||||
write := func(v any) {
|
||||
require.NoError(tb, binary.Write(&body, bo, v))
|
||||
}
|
||||
|
||||
write(d.def)
|
||||
|
||||
if d.withInstance {
|
||||
write(d.instance)
|
||||
write(d.instanceName)
|
||||
body.Write(make([]byte, int(d.instance.ByteLength)-int(perfInstanceDefinitionSize)-len(d.instanceName)*2))
|
||||
}
|
||||
|
||||
write(d.block)
|
||||
body.Write(make([]byte, 4))
|
||||
write(d.value)
|
||||
|
||||
object := d.object
|
||||
if object.TotalByteLength == 0 {
|
||||
object.TotalByteLength = uint32(perfObjectTypeSize) + uint32(body.Len())
|
||||
}
|
||||
|
||||
header := d.header
|
||||
if header.TotalByteLength == 0 {
|
||||
header.TotalByteLength = uint32(perfDataBlockSize) + object.TotalByteLength
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
require.NoError(tb, binary.Write(&buf, bo, header))
|
||||
require.NoError(tb, binary.Write(&buf, bo, object))
|
||||
buf.Write(body.Bytes())
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func TestParsePerformanceData(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, withInstance := range []bool{false, true} {
|
||||
objects, err := parsePerformanceData(newSyntheticPerfData(withInstance).bytes(t), "")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, objects, 1)
|
||||
require.Len(t, objects[0].Instances, 1)
|
||||
require.Len(t, objects[0].Instances[0].Counters, 1)
|
||||
require.Equal(t, int64(42), objects[0].Instances[0].Counters[0].Value)
|
||||
|
||||
if withInstance {
|
||||
require.Equal(t, "inst", objects[0].Instances[0].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePerformanceDataMalformed checks that inconsistent performance data
|
||||
// is reported as an error. The parser runs during Build of the process
|
||||
// collector, where a panic would end the exporter.
|
||||
func TestParsePerformanceDataMalformed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
withInstance bool
|
||||
mutate func(d *syntheticPerfData)
|
||||
}{
|
||||
{"invalid signature", false, func(d *syntheticPerfData) { d.header.Signature[0] = 'X' }},
|
||||
{"header length too short", false, func(d *syntheticPerfData) { d.header.HeaderLength = 0 }},
|
||||
{"too many objects", false, func(d *syntheticPerfData) { d.header.NumObjectTypes = 1 << 31 }},
|
||||
{"too many counters", false, func(d *syntheticPerfData) { d.object.NumCounters = 1 << 30 }},
|
||||
{"too many instances", true, func(d *syntheticPerfData) { d.object.NumInstances = 1 << 30 }},
|
||||
{"counter offset out of range", false, func(d *syntheticPerfData) { d.def.CounterOffset = 1 << 31 }},
|
||||
{"counter offset out of range with instance", true, func(d *syntheticPerfData) { d.def.CounterOffset = 1 << 20 }},
|
||||
{"instance name out of range", true, func(d *syntheticPerfData) { d.instance.NameLength = 1 << 31 }},
|
||||
{"empty counter block", true, func(d *syntheticPerfData) { d.block.ByteLength = 0 }},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
d := newSyntheticPerfData(tc.withInstance)
|
||||
tc.mutate(d)
|
||||
|
||||
buffer := d.bytes(t)
|
||||
|
||||
var err error
|
||||
|
||||
require.NotPanics(t, func() { _, err = parsePerformanceData(buffer, "") })
|
||||
require.ErrorIs(t, err, errMalformedPerformanceData)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePerformanceDataTruncated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, withInstance := range []bool{false, true} {
|
||||
buffer := newSyntheticPerfData(withInstance).bytes(t)
|
||||
|
||||
for n := range buffer {
|
||||
var err error
|
||||
|
||||
require.NotPanics(t, func() { _, err = parsePerformanceData(buffer[:n], "") }, "length %d", n)
|
||||
require.Error(t, err, "length %d", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sharedValuePerfData returns a consistent PERF_DATA_BLOCK with one object
|
||||
// whose counters all read the same 4 bytes of each minimal counter block.
|
||||
func sharedValuePerfData(tb testing.TB, numCounters, numInstances uint32) []byte {
|
||||
tb.Helper()
|
||||
|
||||
var body bytes.Buffer
|
||||
|
||||
write := func(v any) {
|
||||
require.NoError(tb, binary.Write(&body, bo, v))
|
||||
}
|
||||
|
||||
for range numCounters {
|
||||
write(perfCounterDefinition{
|
||||
ByteLength: uint32(perfCounterDefinitionSize),
|
||||
CounterType: pdh.PERF_COUNTER_RAWCOUNT,
|
||||
CounterSize: 4,
|
||||
})
|
||||
}
|
||||
|
||||
for range numInstances {
|
||||
write(perfInstanceDefinition{ByteLength: uint32(perfInstanceDefinitionSize)})
|
||||
write(perfCounterBlock{ByteLength: uint32(perfCounterBlockSize)})
|
||||
}
|
||||
|
||||
object := perfObjectType{
|
||||
TotalByteLength: uint32(perfObjectTypeSize) + uint32(body.Len()),
|
||||
DefinitionLength: uint32(perfObjectTypeSize) + numCounters*uint32(perfCounterDefinitionSize),
|
||||
HeaderLength: uint32(perfObjectTypeSize),
|
||||
NumCounters: numCounters,
|
||||
NumInstances: int32(numInstances),
|
||||
}
|
||||
|
||||
header := perfDataBlock{
|
||||
Signature: [4]uint16{'P', 'E', 'R', 'F'},
|
||||
LittleEndian: 1,
|
||||
TotalByteLength: uint32(perfDataBlockSize) + object.TotalByteLength,
|
||||
HeaderLength: uint32(perfDataBlockSize),
|
||||
NumObjectTypes: 1,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
require.NoError(tb, binary.Write(&buf, bo, header))
|
||||
require.NoError(tb, binary.Write(&buf, bo, object))
|
||||
buf.Write(body.Bytes())
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// TestParsePerformanceDataCounterProduct checks that the counters of all
|
||||
// instances, which share one allocation, are bounded by the buffer size. Data
|
||||
// whose counter values overlap could otherwise request an allocation that
|
||||
// grows with the square of the buffer size.
|
||||
func TestParsePerformanceDataCounterProduct(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
objects, err := parsePerformanceData(sharedValuePerfData(t, 200, 10), "")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, objects[0].Instances, 10)
|
||||
require.Len(t, objects[0].Instances[9].Counters, 200)
|
||||
require.Equal(t, perfCounterBlockSize, objects[0].Instances[9].Counters[199].Value)
|
||||
|
||||
_, err = parsePerformanceData(sharedValuePerfData(t, 200, 200), "")
|
||||
require.ErrorIs(t, err, errMalformedPerformanceData)
|
||||
require.ErrorContains(t, err, "200 instances with 200 counters each")
|
||||
}
|
||||
|
||||
// TestParsePerformanceDataLive parses the data of a real object.
|
||||
func TestParsePerformanceDataLive(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
buffer := livePerformanceData(t, "Process")
|
||||
|
||||
objects, err := parsePerformanceData(buffer, "Process")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, objects, 1)
|
||||
require.NotEmpty(t, objects[0].Instances)
|
||||
require.NotEmpty(t, objects[0].CounterDefs)
|
||||
}
|
||||
|
||||
// TestParsePerformanceDataNoAlias checks that the parsed objects do not
|
||||
// reference the buffer, which Collector.Collect reuses for the next query.
|
||||
func TestParsePerformanceDataNoAlias(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, buffer := range [][]byte{
|
||||
newSyntheticPerfData(true).bytes(t),
|
||||
livePerformanceData(t, "Process"),
|
||||
} {
|
||||
objects, err := parsePerformanceData(buffer, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, err := parsePerformanceData(bytes.Clone(buffer), "")
|
||||
require.NoError(t, err)
|
||||
|
||||
for i := range buffer {
|
||||
buffer[i] = 0xFF
|
||||
}
|
||||
|
||||
require.Equal(t, expected, objects)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryRawDataReusesBuffer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.NoError(t, CounterNameTable.load())
|
||||
|
||||
query := MapCounterToIndex("Process")
|
||||
|
||||
buffer, err := queryRawData(query, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
reused, err := queryRawData(query, buffer)
|
||||
require.NoError(t, err)
|
||||
require.Same(t, unsafe.SliceData(buffer), unsafe.SliceData(reused))
|
||||
|
||||
// A buffer below the initial size guess is not used.
|
||||
small := make([]byte, 16)
|
||||
|
||||
buffer, err = queryRawData(query, small)
|
||||
require.NoError(t, err)
|
||||
require.NotSame(t, unsafe.SliceData(small), unsafe.SliceData(buffer))
|
||||
}
|
||||
|
||||
func livePerformanceData(tb testing.TB, object string) []byte {
|
||||
tb.Helper()
|
||||
|
||||
require.NoError(tb, CounterNameTable.load())
|
||||
|
||||
buffer, err := queryRawData(MapCounterToIndex(object), nil)
|
||||
require.NoError(tb, err)
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
func FuzzParsePerformanceData(f *testing.F) {
|
||||
f.Add(newSyntheticPerfData(false).bytes(f))
|
||||
f.Add(newSyntheticPerfData(true).bytes(f))
|
||||
f.Add(livePerformanceData(f, "System"))
|
||||
f.Add([]byte{})
|
||||
|
||||
f.Fuzz(func(_ *testing.T, data []byte) {
|
||||
// Must not panic on any input.
|
||||
_, _ = parsePerformanceData(data, "")
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseNameTable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
multiSZ := func(s ...string) []byte {
|
||||
var buf bytes.Buffer
|
||||
|
||||
for _, v := range s {
|
||||
require.NoError(t, binary.Write(&buf, bo, windows.StringToUTF16(v)))
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
index, names := parseNameTable(multiSZ("1", "1847", "2", "System", "4", "Memory"))
|
||||
require.Equal(t, map[uint32]string{1: "1847", 2: "System", 4: "Memory"}, index)
|
||||
require.Equal(t, uint32(4), names["Memory"])
|
||||
|
||||
// An incomplete pair and an odd trailing byte are ignored.
|
||||
buffer := append(multiSZ("2", "System", "4"), 'M')
|
||||
index, _ = parseNameTable(buffer)
|
||||
require.Equal(t, map[uint32]string{2: "System"}, index)
|
||||
|
||||
index, _ = parseNameTable(nil)
|
||||
require.Empty(t, index)
|
||||
}
|
||||
|
||||
func TestNewCollectorUnknownObject(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type counterValues struct {
|
||||
Name string
|
||||
|
||||
ProcessorQueueLength float64 `perfdata:"Processor Queue Length"`
|
||||
}
|
||||
|
||||
_, err := NewCollector[counterValues]("Object That Does Not Exist", nil)
|
||||
require.ErrorContains(t, err, "not found in the counter name table")
|
||||
}
|
||||
|
||||
// BenchmarkParsePerformanceData measures the parser alone, without the
|
||||
// RegQueryValueEx call that dominates BenchmarkCollectorCollect.
|
||||
func BenchmarkParsePerformanceData(b *testing.B) {
|
||||
buffer := livePerformanceData(b, "Process")
|
||||
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if _, err := parsePerformanceData(buffer, "Process"); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecodeMatchesBinaryRead checks the hand-written decoders of the raw
|
||||
// structures against binary.Read.
|
||||
func TestDecodeMatchesBinaryRead(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type decoder interface {
|
||||
decode(b []byte) error
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
decoded, expected decoder
|
||||
}{
|
||||
{new(perfDataBlock), new(perfDataBlock)},
|
||||
{new(perfObjectType), new(perfObjectType)},
|
||||
{new(perfCounterDefinition), new(perfCounterDefinition)},
|
||||
{new(perfCounterBlock), new(perfCounterBlock)},
|
||||
{new(perfInstanceDefinition), new(perfInstanceDefinition)},
|
||||
} {
|
||||
// Distinct bytes, so a field read from the wrong offset gets a different value.
|
||||
buffer := make([]byte, binary.Size(tc.expected))
|
||||
for i := range buffer {
|
||||
buffer[i] = byte(i + 1)
|
||||
}
|
||||
|
||||
require.NoError(t, binary.Read(bytes.NewReader(buffer), bo, tc.expected))
|
||||
require.NoError(t, tc.decoded.decode(buffer))
|
||||
require.Equal(t, tc.expected, tc.decoded)
|
||||
|
||||
require.ErrorIs(t, tc.decoded.decode(buffer[:len(buffer)-1]), io.ErrUnexpectedEOF)
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestNewCollectorStructTypeParam guards against a regression where
|
||||
// reflect.TypeFor[T]().Elem() panicked when T is a plain struct (not a pointer).
|
||||
// See https://github.com/prometheus-community/windows_exporter/issues/2365
|
||||
func TestNewCollectorStructTypeParam(t *testing.T) {
|
||||
type systemCounterValues struct {
|
||||
Name string
|
||||
|
||||
ProcessorQueueLength float64 `perfdata:"Processor Queue Length"`
|
||||
}
|
||||
|
||||
_, err := NewCollector[systemCounterValues]("System", nil)
|
||||
if err != nil {
|
||||
t.Skipf("skipping: failed to create collector: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewCollectorNonStruct guards against a regression where NewCollector
|
||||
// panicked instead of returning an error for a type parameter that is not a struct.
|
||||
func TestNewCollectorNonStruct(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if _, err := NewCollector[int]("System", nil); err == nil {
|
||||
t.Error("expected an error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectNilDestination(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type counterValues struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
if err := (&Collector[counterValues]{}).Collect(nil); err == nil {
|
||||
t.Error("expected an error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectConcurrent runs Collect concurrently on one collector, as
|
||||
// overlapping scrapes do, to let the race detector check the buffer reuse.
|
||||
func TestCollectConcurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type systemCounterValues struct {
|
||||
Name string
|
||||
|
||||
ProcessorQueueLength float64 `perfdata:"Processor Queue Length"`
|
||||
}
|
||||
|
||||
collector, err := NewCollector[systemCounterValues]("System", nil)
|
||||
if err != nil {
|
||||
t.Skipf("skipping: failed to create collector: %v", err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for range 8 {
|
||||
wg.Go(func() {
|
||||
for range 10 {
|
||||
var dst []systemCounterValues
|
||||
|
||||
if err := collector.Collect(&dst); err != nil {
|
||||
t.Error(err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(dst) != 1 {
|
||||
t.Errorf("expected 1 row, got %d", len(dst))
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func BenchmarkQueryPerformanceData(b *testing.B) {
|
||||
for b.Loop() {
|
||||
_, _ = QueryPerformanceData("Global", "")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCollectorCollect(b *testing.B) {
|
||||
type processCounterValues struct {
|
||||
Name string
|
||||
|
||||
ProcessID float64 `perfdata:"ID Process"`
|
||||
ProcessorTime float64 `perfdata:"% Processor Time"`
|
||||
WorkingSet float64 `perfdata:"Working Set"`
|
||||
HandleCount float64 `perfdata:"Handle Count"`
|
||||
ThreadCount float64 `perfdata:"Thread Count"`
|
||||
IOReadBytesSec float64 `perfdata:"IO Read Bytes/sec"`
|
||||
}
|
||||
|
||||
collector, err := NewCollector[processCounterValues]("Process", nil)
|
||||
if err != nil {
|
||||
b.Skipf("skipping: failed to create collector: %v", err)
|
||||
}
|
||||
|
||||
var dst []processCounterValues
|
||||
|
||||
b.ReportAllocs()
|
||||
|
||||
for b.Loop() {
|
||||
if err := collector.Collect(&dst); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
/*
|
||||
perfDataBlock
|
||||
See: https://msdn.microsoft.com/de-de/library/windows/desktop/aa373157(v=vs.85).aspx
|
||||
|
||||
typedef struct _PERF_DATA_BLOCK {
|
||||
WCHAR Signature[4];
|
||||
DWORD LittleEndian;
|
||||
DWORD Version;
|
||||
DWORD Revision;
|
||||
DWORD TotalByteLength;
|
||||
DWORD HeaderLength;
|
||||
DWORD NumObjectTypes;
|
||||
DWORD DefaultObject;
|
||||
SYSTEMTIME SystemTime;
|
||||
LARGE_INTEGER PerfTime;
|
||||
LARGE_INTEGER PerfFreq;
|
||||
LARGE_INTEGER PerfTime100nSec;
|
||||
DWORD SystemNameLength;
|
||||
DWORD SystemNameOffset;
|
||||
} PERF_DATA_BLOCK;
|
||||
*/
|
||||
type perfDataBlock struct {
|
||||
Signature [4]uint16
|
||||
LittleEndian uint32
|
||||
Version uint32
|
||||
Revision uint32
|
||||
TotalByteLength uint32
|
||||
HeaderLength uint32
|
||||
NumObjectTypes uint32
|
||||
DefaultObject int32
|
||||
SystemTime windows.Systemtime
|
||||
_ uint32 // unknown field
|
||||
PerfTime int64
|
||||
PerfFreq int64
|
||||
PerfTime100nSec int64
|
||||
SystemNameLength uint32
|
||||
SystemNameOffset uint32
|
||||
}
|
||||
|
||||
// decode reads p from the start of b. The fields are decoded directly, since
|
||||
// binary.Read allocates and walks the struct by reflection on every call.
|
||||
func (p *perfDataBlock) decode(b []byte) error {
|
||||
if int64(len(b)) < perfDataBlockSize {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
*p = perfDataBlock{
|
||||
Signature: [4]uint16{bo.Uint16(b[0:]), bo.Uint16(b[2:]), bo.Uint16(b[4:]), bo.Uint16(b[6:])},
|
||||
LittleEndian: bo.Uint32(b[8:]),
|
||||
Version: bo.Uint32(b[12:]),
|
||||
Revision: bo.Uint32(b[16:]),
|
||||
TotalByteLength: bo.Uint32(b[20:]),
|
||||
HeaderLength: bo.Uint32(b[24:]),
|
||||
NumObjectTypes: bo.Uint32(b[28:]),
|
||||
DefaultObject: int32(bo.Uint32(b[32:])),
|
||||
SystemTime: windows.Systemtime{
|
||||
Year: bo.Uint16(b[36:]),
|
||||
Month: bo.Uint16(b[38:]),
|
||||
DayOfWeek: bo.Uint16(b[40:]),
|
||||
Day: bo.Uint16(b[42:]),
|
||||
Hour: bo.Uint16(b[44:]),
|
||||
Minute: bo.Uint16(b[46:]),
|
||||
Second: bo.Uint16(b[48:]),
|
||||
Milliseconds: bo.Uint16(b[50:]),
|
||||
},
|
||||
PerfTime: int64(bo.Uint64(b[56:])),
|
||||
PerfFreq: int64(bo.Uint64(b[64:])),
|
||||
PerfTime100nSec: int64(bo.Uint64(b[72:])),
|
||||
SystemNameLength: bo.Uint32(b[80:]),
|
||||
SystemNameOffset: bo.Uint32(b[84:]),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
perfObjectType
|
||||
See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa373160(v=vs.85).aspx
|
||||
|
||||
typedef struct _PERF_OBJECT_TYPE {
|
||||
DWORD TotalByteLength;
|
||||
DWORD DefinitionLength;
|
||||
DWORD HeaderLength;
|
||||
DWORD ObjectNameTitleIndex;
|
||||
LPWSTR ObjectNameTitle;
|
||||
DWORD ObjectHelpTitleIndex;
|
||||
LPWSTR ObjectHelpTitle;
|
||||
DWORD DetailLevel;
|
||||
DWORD NumCounters;
|
||||
DWORD DefaultCounter;
|
||||
DWORD NumInstances;
|
||||
DWORD CodePage;
|
||||
LARGE_INTEGER PerfTime;
|
||||
LARGE_INTEGER PerfFreq;
|
||||
} PERF_OBJECT_TYPE;
|
||||
*/
|
||||
type perfObjectType struct {
|
||||
TotalByteLength uint32
|
||||
DefinitionLength uint32
|
||||
HeaderLength uint32
|
||||
ObjectNameTitleIndex uint32
|
||||
ObjectNameTitle uint32
|
||||
ObjectHelpTitleIndex uint32
|
||||
ObjectHelpTitle uint32
|
||||
DetailLevel uint32
|
||||
NumCounters uint32
|
||||
DefaultCounter int32
|
||||
NumInstances int32
|
||||
CodePage uint32
|
||||
PerfTime int64
|
||||
PerfFreq int64
|
||||
}
|
||||
|
||||
// decode reads p from the start of b.
|
||||
func (p *perfObjectType) decode(b []byte) error {
|
||||
if int64(len(b)) < perfObjectTypeSize {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
*p = perfObjectType{
|
||||
TotalByteLength: bo.Uint32(b[0:]),
|
||||
DefinitionLength: bo.Uint32(b[4:]),
|
||||
HeaderLength: bo.Uint32(b[8:]),
|
||||
ObjectNameTitleIndex: bo.Uint32(b[12:]),
|
||||
ObjectNameTitle: bo.Uint32(b[16:]),
|
||||
ObjectHelpTitleIndex: bo.Uint32(b[20:]),
|
||||
ObjectHelpTitle: bo.Uint32(b[24:]),
|
||||
DetailLevel: bo.Uint32(b[28:]),
|
||||
NumCounters: bo.Uint32(b[32:]),
|
||||
DefaultCounter: int32(bo.Uint32(b[36:])),
|
||||
NumInstances: int32(bo.Uint32(b[40:])),
|
||||
CodePage: bo.Uint32(b[44:]),
|
||||
PerfTime: int64(bo.Uint64(b[48:])),
|
||||
PerfFreq: int64(bo.Uint64(b[56:])),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
perfCounterDefinition
|
||||
See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa373150(v=vs.85).aspx
|
||||
|
||||
typedef struct _PERF_COUNTER_DEFINITION {
|
||||
DWORD ByteLength;
|
||||
DWORD CounterNameTitleIndex;
|
||||
LPWSTR CounterNameTitle;
|
||||
DWORD CounterHelpTitleIndex;
|
||||
LPWSTR CounterHelpTitle;
|
||||
LONG DefaultScale;
|
||||
DWORD DetailLevel;
|
||||
DWORD CounterType;
|
||||
DWORD CounterSize;
|
||||
DWORD CounterOffset;
|
||||
} PERF_COUNTER_DEFINITION;
|
||||
*/
|
||||
type perfCounterDefinition struct {
|
||||
ByteLength uint32
|
||||
CounterNameTitleIndex uint32
|
||||
CounterNameTitle uint32
|
||||
CounterHelpTitleIndex uint32
|
||||
CounterHelpTitle uint32
|
||||
DefaultScale int32
|
||||
DetailLevel uint32
|
||||
CounterType uint32
|
||||
CounterSize uint32
|
||||
CounterOffset uint32
|
||||
}
|
||||
|
||||
// decode reads p from the start of b.
|
||||
func (p *perfCounterDefinition) decode(b []byte) error {
|
||||
if int64(len(b)) < perfCounterDefinitionSize {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
*p = perfCounterDefinition{
|
||||
ByteLength: bo.Uint32(b[0:]),
|
||||
CounterNameTitleIndex: bo.Uint32(b[4:]),
|
||||
CounterNameTitle: bo.Uint32(b[8:]),
|
||||
CounterHelpTitleIndex: bo.Uint32(b[12:]),
|
||||
CounterHelpTitle: bo.Uint32(b[16:]),
|
||||
DefaultScale: int32(bo.Uint32(b[20:])),
|
||||
DetailLevel: bo.Uint32(b[24:]),
|
||||
CounterType: bo.Uint32(b[28:]),
|
||||
CounterSize: bo.Uint32(b[32:]),
|
||||
CounterOffset: bo.Uint32(b[36:]),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *perfCounterDefinition) LookupName() string {
|
||||
return CounterNameTable.LookupString(p.CounterNameTitleIndex)
|
||||
}
|
||||
|
||||
/*
|
||||
perfCounterBlock
|
||||
See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa373147(v=vs.85).aspx
|
||||
|
||||
typedef struct _PERF_COUNTER_BLOCK {
|
||||
DWORD ByteLength;
|
||||
} PERF_COUNTER_BLOCK;
|
||||
*/
|
||||
type perfCounterBlock struct {
|
||||
ByteLength uint32
|
||||
}
|
||||
|
||||
// decode reads p from the start of b.
|
||||
func (p *perfCounterBlock) decode(b []byte) error {
|
||||
if int64(len(b)) < perfCounterBlockSize {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
p.ByteLength = bo.Uint32(b)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
perfInstanceDefinition
|
||||
See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa373159(v=vs.85).aspx
|
||||
|
||||
typedef struct _PERF_INSTANCE_DEFINITION {
|
||||
DWORD ByteLength;
|
||||
DWORD ParentObjectTitleIndex;
|
||||
DWORD ParentObjectInstance;
|
||||
DWORD UniqueID;
|
||||
DWORD NameOffset;
|
||||
DWORD NameLength;
|
||||
} PERF_INSTANCE_DEFINITION;
|
||||
*/
|
||||
type perfInstanceDefinition struct {
|
||||
ByteLength uint32
|
||||
ParentObjectTitleIndex uint32
|
||||
ParentObjectInstance uint32
|
||||
UniqueID uint32
|
||||
NameOffset uint32
|
||||
NameLength uint32
|
||||
}
|
||||
|
||||
// decode reads p from the start of b.
|
||||
func (p *perfInstanceDefinition) decode(b []byte) error {
|
||||
if int64(len(b)) < perfInstanceDefinitionSize {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
*p = perfInstanceDefinition{
|
||||
ByteLength: bo.Uint32(b[0:]),
|
||||
ParentObjectTitleIndex: bo.Uint32(b[4:]),
|
||||
ParentObjectInstance: bo.Uint32(b[8:]),
|
||||
UniqueID: bo.Uint32(b[12:]),
|
||||
NameOffset: bo.Uint32(b[16:]),
|
||||
NameLength: bo.Uint32(b[20:]),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// decodeUTF16String decodes the unterminated UTF16 string b up to the first NUL.
|
||||
// A trailing odd byte is ignored. scratch holds the code units; it is returned
|
||||
// with any growth so the caller can reuse it for the next string. The returned
|
||||
// string does not reference b.
|
||||
func decodeUTF16String(b []byte, scratch []uint16) (string, []uint16) {
|
||||
scratch = scratch[:0]
|
||||
|
||||
for i := 0; i+1 < len(b); i += 2 {
|
||||
scratch = append(scratch, bo.Uint16(b[i:]))
|
||||
}
|
||||
|
||||
return windows.UTF16ToString(scratch), scratch
|
||||
}
|
||||
|
||||
// readUTF16String Reads a null-terminated UTF16 string at the current offset.
|
||||
func readUTF16String(r io.Reader) (string, error) {
|
||||
b := make([]byte, 2)
|
||||
out := make([]uint16, 0, 100)
|
||||
|
||||
for {
|
||||
// A short read at the end of the buffer is an error, not a code unit.
|
||||
if _, err := io.ReadFull(r, b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if b[0] == 0 && b[1] == 0 {
|
||||
return windows.UTF16ToString(out), nil
|
||||
}
|
||||
|
||||
out = append(out, bo.Uint16(b))
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func MapCounterToIndex(name string) string {
|
||||
return strconv.Itoa(int(CounterNameTable.LookupIndex(name)))
|
||||
}
|
||||
Reference in New Issue
Block a user