Refactor cpu collector to use perflib instead of WMI.

This commit is contained in:
Calle Pettersson
2019-04-05 15:59:40 +02:00
parent 462a136673
commit 33879449a2
42 changed files with 1681 additions and 234 deletions

21
vendor/github.com/leoluk/perflib_exporter/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
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.

View File

@@ -0,0 +1,184 @@
package collector
import (
"strings"
"github.com/leoluk/perflib_exporter/perflib"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/log"
)
// ...
const (
Namespace = "perflib"
// Conversion factors
hundredNsToSecondsScaleFactor = 1 / 1e7
)
// Collector is the interface a collector has to implement.
type Collector interface {
// Get new metrics and expose them via prometheus registry.
Collect(ch chan<- prometheus.Metric) (err error)
}
type CounterKey struct {
ObjectIndex uint
CounterIndex uint
CounterType uint32 // This is a bit mask
}
func NewCounterKey(object *perflib.PerfObject, def *perflib.PerfCounterDef) CounterKey {
return CounterKey{object.NameIndex, def.NameIndex, def.CounterType}
}
type PerflibCollector struct {
perflibQuery string
perflibDescs map[CounterKey]*prometheus.Desc
}
func NewPerflibCollector(query string) (c PerflibCollector) {
c.perflibQuery = query
objects, err := perflib.QueryPerformanceData(c.perflibQuery)
if err != nil {
panic(err)
}
log.Debugf("Number of objects: %d", len(objects))
c.perflibDescs = make(map[CounterKey]*prometheus.Desc)
for _, object := range objects {
for _, def := range object.CounterDefs {
desc := descFromCounterDef(*object, *def)
key := NewCounterKey(object, def)
c.perflibDescs[key] = desc
}
}
return
}
func (c PerflibCollector) Collect(ch chan<- prometheus.Metric) (err error) {
// TODO QueryPerformanceData timing metric
objects, err := perflib.QueryPerformanceData(c.perflibQuery)
if err != nil {
// TODO - we shouldn't panic if a single call fails
panic(err)
}
log.Debugf("Number of objects: %d", len(objects))
for _, object := range objects {
n := object.NameIndex
for _, instance := range object.Instances {
name := instance.Name
// _Total metrics do not fit into the Prometheus model - we try to merge similar
// metrics and give them labels, so you'd sum() them instead. Having a _Total label
// would make
if strings.HasSuffix(name, "_Total") || strings.HasPrefix(name, "Total") {
continue
}
for _, counter := range instance.Counters {
if IsDefPromotedLabel(n, counter.Def.NameIndex) {
continue
}
if counter == nil {
log.Debugf("nil counter for %s -> %s", object.Name, instance.Name)
continue
}
if counter.Def.NameIndex == 0 {
log.Debugf("null counter index for %s -> %s", object.Name, instance.Name)
continue
}
if counter.Def.Name == "" {
log.Debugf("no counter name for %s -> %s", object.Name, instance.Name)
continue
}
if counter.Def.Name == "No name" {
log.Debugf("no name counter %s -> %s -> %s", object.Name, instance.Name, counter.Def.Name)
continue
}
key := NewCounterKey(object, counter.Def)
desc, ok := c.perflibDescs[key]
if !ok {
log.Debugf("missing metric description for counter %s -> %s -> %s", object.Name, instance.Name, counter.Def.Name)
continue
}
labels := []string{name}
if len(object.Instances) == 1 {
labels = []string{}
}
if HasPromotedLabels(n) {
labels = append(labels, PromotedLabelValuesForInstance(n, instance)...)
}
// TODO - Label merging needs to be fixed for [230] Process
//if HasMergedLabels(n) {
// _, value := MergedMetricForInstance(n, counter.Def.NameIndex)
//
// // Null string in definition means we should skip this metric (it's probably a sum)
// if value == "" {
// log.Debugf("Skipping %d -> %s (empty merge label)", n, counter.Def.NameIndex)
// continue
// }
// labels = append(labels, value)
//}
valueType, err := GetPrometheusValueType(counter.Def.CounterType)
if err != nil {
// TODO - Is this too verbose? There will always be counter types we don't support
log.Debug(err)
continue
}
value := float64(counter.Value)
if counter.Def.IsNanosecondCounter {
value = value * hundredNsToSecondsScaleFactor
}
if IsElapsedTime(counter.Def.CounterType) {
// convert from Windows timestamp (1 jan 1601) to unix timestamp (1 jan 1970)
value = float64(counter.Value-116444736000000000) / float64(object.Frequency)
}
metric := prometheus.MustNewConstMetric(
desc,
valueType,
value,
labels...,
)
ch <- metric
}
}
}
/*ch <- prometheus.MustNewConstMetric(
prometheus.NewDesc(),
prometheus.CounterValue,
float64(0),
"ds_client",
)*/
return nil
}

View File

@@ -0,0 +1,90 @@
package collector
import (
"fmt"
"strings"
"github.com/leoluk/perflib_exporter/perflib"
"github.com/prometheus/client_golang/prometheus"
)
func manglePerflibName(s string) string {
s = strings.ToLower(s)
s = strings.Replace(s, " ", "_", -1)
s = strings.Replace(s, ".", "", -1)
s = strings.Replace(s, "(", "", -1)
s = strings.Replace(s, ")", "", -1)
s = strings.Replace(s, "+", "", -1)
s = strings.Replace(s, "-", "", -1)
s = strings.Replace(s, ",", "", -1)
return s
}
func manglePerflibCounterName(s string) string {
s = manglePerflibName(s)
s = strings.Replace(s, "total_", "", -1)
s = strings.Replace(s, "_total", "", -1)
s = strings.Replace(s, "/second", "", -1)
s = strings.Replace(s, "/sec", "", -1)
s = strings.Replace(s, "_%", "", -1)
s = strings.Replace(s, "%_", "", -1)
s = strings.Replace(s, "/", "_per_", -1)
s = strings.Replace(s, "&", "and", -1)
s = strings.Replace(s, "#_of_", "", -1)
s = strings.Replace(s, ":", "", -1)
s = strings.Replace(s, "__", "_", -1)
s = strings.Trim(s, " _")
return s
}
func MakePrometheusLabel(def *perflib.PerfCounterDef) (s string) {
s = manglePerflibCounterName(def.Name)
if len(s) > 0 {
if IsCounter(def.CounterType) {
s += "_total"
} else if IsBaseValue(def.CounterType) && !strings.HasSuffix(s, "_base") {
s += "_max"
}
}
return
}
func pdhNameFromCounterDef(obj perflib.PerfObject, def perflib.PerfCounterDef) string {
return fmt.Sprintf(`\%s(*)\%s`, obj.Name, def.Name)
}
func descFromCounterDef(obj perflib.PerfObject, def perflib.PerfCounterDef) *prometheus.Desc {
subsystem := manglePerflibName(obj.Name)
counterName := MakePrometheusLabel(&def)
labels := []string{"name"}
if len(obj.Instances) == 1 {
labels = []string{}
}
if HasPromotedLabels(obj.NameIndex) {
labels = append(labels, PromotedLabelsForObject(obj.NameIndex)...)
}
// TODO - Label merging needs to be fixed for [230] Process
//if HasMergedLabels(obj.NameIndex) {
// s, labelsForObject := MergedLabelsForInstance(obj.NameIndex, def.NameIndex)
// counterName = s
// labels = append(labels, labelsForObject)
//}
return prometheus.NewDesc(
prometheus.BuildFQName(Namespace, subsystem, counterName),
fmt.Sprintf("perflib metric: %s (see /dump for docs) [%d]",
pdhNameFromCounterDef(obj, def), def.NameIndex),
labels,
nil,
)
}

View File

@@ -0,0 +1,91 @@
package collector
import (
"fmt"
"github.com/prometheus/client_golang/prometheus"
)
const (
PERF_COUNTER_RAWCOUNT_HEX = 0x00000000
PERF_COUNTER_LARGE_RAWCOUNT_HEX = 0x00000100
PERF_COUNTER_TEXT = 0x00000b00
PERF_COUNTER_RAWCOUNT = 0x00010000
PERF_COUNTER_LARGE_RAWCOUNT = 0x00010100
PERF_DOUBLE_RAW = 0x00012000
PERF_COUNTER_DELTA = 0x00400400
PERF_COUNTER_LARGE_DELTA = 0x00400500
PERF_SAMPLE_COUNTER = 0x00410400
PERF_COUNTER_QUEUELEN_TYPE = 0x00450400
PERF_COUNTER_LARGE_QUEUELEN_TYPE = 0x00450500
PERF_COUNTER_100NS_QUEUELEN_TYPE = 0x00550500
PERF_COUNTER_OBJ_TIME_QUEUELEN_TYPE = 0x00650500
PERF_COUNTER_COUNTER = 0x10410400
PERF_COUNTER_BULK_COUNT = 0x10410500
PERF_RAW_FRACTION = 0x20020400
PERF_LARGE_RAW_FRACTION = 0x20020500
PERF_COUNTER_TIMER = 0x20410500
PERF_PRECISION_SYSTEM_TIMER = 0x20470500
PERF_100NSEC_TIMER = 0x20510500
PERF_PRECISION_100NS_TIMER = 0x20570500
PERF_OBJ_TIME_TIMER = 0x20610500
PERF_PRECISION_OBJECT_TIMER = 0x20670500
PERF_SAMPLE_FRACTION = 0x20c20400
PERF_COUNTER_TIMER_INV = 0x21410500
PERF_100NSEC_TIMER_INV = 0x21510500
PERF_COUNTER_MULTI_TIMER = 0x22410500
PERF_100NSEC_MULTI_TIMER = 0x22510500
PERF_COUNTER_MULTI_TIMER_INV = 0x23410500
PERF_100NSEC_MULTI_TIMER_INV = 0x23510500
PERF_AVERAGE_TIMER = 0x30020400
PERF_ELAPSED_TIME = 0x30240500
PERF_COUNTER_NODATA = 0x40000200
PERF_AVERAGE_BULK = 0x40020500
PERF_SAMPLE_BASE = 0x40030401
PERF_AVERAGE_BASE = 0x40030402
PERF_RAW_BASE = 0x40030403
PERF_PRECISION_TIMESTAMP = 0x40030500
PERF_LARGE_RAW_BASE = 0x40030503
PERF_COUNTER_MULTI_BASE = 0x42030500
PERF_COUNTER_HISTOGRAM_TYPE = 0x80000000
)
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,
}
func IsCounter(counterType uint32) bool {
return supportedCounterTypes[counterType] == prometheus.CounterValue
}
func IsBaseValue(counterType uint32) bool {
return counterType == PERF_SAMPLE_BASE || counterType == PERF_RAW_BASE || counterType == PERF_LARGE_RAW_BASE
}
func IsElapsedTime(counterType uint32) bool {
return counterType == PERF_ELAPSED_TIME
}
func GetPrometheusValueType(counterType uint32) (prometheus.ValueType, error) {
val, ok := supportedCounterTypes[counterType]
if !ok {
return 0, fmt.Errorf("counter type %#08x is not supported", counterType)
}
return val, nil
}

View File

@@ -0,0 +1,38 @@
package collector
import "fmt"
var mergedDefinitions = map[uint]map[string]map[uint]string{
230: {
"processor_time_total": {
0: "mode",
6: "", // Processor Time (drop)
142: "user", // User Time
144: "privileged", // Privileged Time
},
},
}
// Return if a given object has merge definitions
func HasMergedLabels(index uint) bool {
_, ok := mergedDefinitions[index]
return ok
}
// Return a list of merged label names for an instance
func MergedLabelsForInstance(objIndex uint, def uint) (name string, labelName string) {
return MergedMetricForInstance(objIndex, 0)
}
// Return merged metric name and label value for an instance
func MergedMetricForInstance(objIndex uint, def uint) (name string, label string) {
for k, v := range mergedDefinitions[objIndex] {
for n := range v {
if def == n {
return k, v[n]
}
}
}
panic(fmt.Sprintf("No merge definition for obj %d, inst %d", objIndex, def))
}

View File

@@ -0,0 +1,56 @@
package collector
import (
"strconv"
"github.com/leoluk/perflib_exporter/perflib"
)
var labelPromotionLabels = map[uint][]string{
230: {
"process_id",
"creating_process_id",
},
}
var labelPromotionValues = map[uint][]uint{
230: {
784, // process_id
1410, // creating_process_id
},
}
// Get a list of promoted labels for an object
func PromotedLabelsForObject(index uint) []string {
return labelPromotionLabels[index]
}
// Get a list of label values for a given object and instance
func PromotedLabelValuesForInstance(index uint, instance *perflib.PerfInstance) []string {
values := make([]string, len(labelPromotionValues[index]))
for _, c := range instance.Counters {
for i, v := range labelPromotionValues[index] {
if c.Def.NameIndex == v {
values[i] = strconv.Itoa(int(c.Value))
}
}
}
return values
}
// Return if a given object has label promotion definitions
func HasPromotedLabels(index uint) bool {
_, ok := labelPromotionLabels[index]
return ok
}
// Return if a given definition is a promoted label for an object
func IsDefPromotedLabel(objIndex uint, def uint) bool {
for _, v := range labelPromotionValues[objIndex] {
if v == def {
return true
}
}
return false
}

View File

@@ -0,0 +1,75 @@
package perflib
import (
"bytes"
"fmt"
"strconv"
)
type nameTableLookuper interface {
LookupName() string
LookupHelp() string
}
func (p *perfObjectType) LookupName() string {
return counterNameTable.LookupString(p.ObjectNameTitleIndex)
}
func (p *perfObjectType) LookupHelp() string {
return helpNameTable.LookupString(p.ObjectHelpTitleIndex)
}
type NameTable struct {
byIndex map[uint32]string
byString map[string]uint32
}
func (t *NameTable) LookupString(index uint32) string {
return t.byIndex[index]
}
func (t *NameTable) LookupIndex(str string) uint32 {
return t.byString[str]
}
// Query a perflib name table from the registry. Specify the type and the language
// code (i.e. "Counter 009" or "Help 009") for English language.
func QueryNameTable(tableName string) *NameTable {
nameTable := new(NameTable)
nameTable.byIndex = make(map[uint32]string)
buffer, err := queryRawData(tableName)
if err != nil {
panic(err)
}
r := bytes.NewReader(buffer)
for {
index, err := readUTF16String(r)
if err != nil {
break
}
desc, err := readUTF16String(r)
if err != nil {
break
}
indexInt, _ := strconv.Atoi(index)
if err != nil {
panic(fmt.Sprint("Invalid index ", index))
}
nameTable.byIndex[uint32(indexInt)] = desc
}
nameTable.byString = make(map[string]uint32)
for k, v := range nameTable.byIndex {
nameTable.byString[v] = k
}
return nameTable
}

View File

@@ -0,0 +1,460 @@
/*
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 registry 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
*/
package perflib
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"sort"
"strings"
"syscall"
"unsafe"
)
// TODO: There's a LittleEndian field in the PERF header - we ought to check it
var bo = binary.LittleEndian
var counterNameTable NameTable
var helpNameTable NameTable
// Top-level performance object (like "Process").
type PerfObject struct {
Name string
// Same index you pass to QueryPerformanceData
NameIndex uint
HelpText string
HelpTextIndex uint
Instances []*PerfInstance
CounterDefs []*PerfCounterDef
Frequency int64
rawData *perfObjectType
}
// 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 []*PerfCounter
rawData *perfInstanceDefinition
rawCounterBlock *perfCounterBlock
}
type PerfCounterDef struct {
Name string
NameIndex uint
HelpText string
HelpTextIndex 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
rawData *perfCounterDefinition
}
type PerfCounter struct {
Value int64
Def *PerfCounterDef
}
// Error value returned by RegQueryValueEx if the buffer isn't sufficiently large
const errorMoreData = syscall.Errno(234)
var (
bufLenGlobal = uint32(400000)
bufLenCostly = uint32(2000000)
)
// 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
func queryRawData(query string) ([]byte, error) {
var (
valType uint32
buffer []byte
bufLen uint32
)
switch query {
case "Global":
bufLen = bufLenGlobal
case "Costly":
bufLen = bufLenCostly
default:
// TODO: depends on the number of values requested
// need make an educated guess
numCounters := len(strings.Split(query, " "))
bufLen = uint32(150000 * numCounters)
}
buffer = make([]byte, bufLen)
name, err := syscall.UTF16PtrFromString(query)
if err != nil {
return nil, fmt.Errorf("failed to encode query string: %v", err)
}
for {
bufLen := uint32(len(buffer))
err := syscall.RegQueryValueEx(
syscall.HKEY_PERFORMANCE_DATA,
name,
nil,
&valType,
(*byte)(unsafe.Pointer(&buffer[0])),
&bufLen)
if err == errorMoreData {
newBuffer := make([]byte, len(buffer)+16384)
copy(newBuffer, buffer)
buffer = newBuffer
continue
} else if err != nil {
if errno, ok := err.(syscall.Errno); ok {
return nil, fmt.Errorf("ReqQueryValueEx failed: %v 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
}
}
func init() {
// Initialize global name tables
// TODO: 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)
counterNameTable = *QueryNameTable("Counter 009")
helpNameTable = *QueryNameTable("Help 009")
}
/*
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) ([]*PerfObject, error) {
buffer, err := queryRawData(query)
if err != nil {
return nil, err
}
r := bytes.NewReader(buffer)
// Read global header
header := new(perfDataBlock)
err = header.BinaryReadFrom(r)
if err != nil {
return nil, err
}
// Check for "PERF" signature
if header.Signature != [4]uint16{80, 69, 82, 70} {
panic("Invalid performance block header")
}
// Parse the performance data
numObjects := int(header.NumObjectTypes)
objects := make([]*PerfObject, numObjects)
objOffset := int64(header.HeaderLength)
for i := 0; i < numObjects; i++ {
r.Seek(objOffset, io.SeekStart)
obj := new(perfObjectType)
obj.BinaryReadFrom(r)
numCounterDefs := int(obj.NumCounters)
numInstances := int(obj.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
}
instances := make([]*PerfInstance, numInstances)
counterDefs := make([]*PerfCounterDef, numCounterDefs)
objects[i] = &PerfObject{
Name: obj.LookupName(),
NameIndex: uint(obj.ObjectNameTitleIndex),
HelpText: obj.LookupHelp(),
HelpTextIndex: uint(obj.ObjectHelpTitleIndex),
Instances: instances,
CounterDefs: counterDefs,
Frequency: obj.PerfFreq,
rawData: obj,
}
for i := 0; i < numCounterDefs; i++ {
def := new(perfCounterDefinition)
def.BinaryReadFrom(r)
counterDefs[i] = &PerfCounterDef{
Name: def.LookupName(),
NameIndex: uint(def.CounterNameTitleIndex),
HelpText: def.LookupHelp(),
HelpTextIndex: uint(def.CounterHelpTitleIndex),
rawData: def,
CounterType: def.CounterType,
IsCounter: def.CounterType&0x400 == 0x400,
IsBaseValue: def.CounterType&0x00030000 == 0x00030000,
IsNanosecondCounter: def.CounterType&0x00100000 == 0x00100000,
}
}
if obj.NumInstances <= 0 {
blockOffset := objOffset + int64(obj.DefinitionLength)
r.Seek(blockOffset, io.SeekStart)
_, counters := parseCounterBlock(buffer, r, blockOffset, counterDefs)
instances[0] = &PerfInstance{
Name: "",
Counters: counters,
rawData: nil,
rawCounterBlock: nil,
}
} else {
instOffset := objOffset + int64(obj.DefinitionLength)
for i := 0; i < numInstances; i++ {
r.Seek(instOffset, io.SeekStart)
inst := new(perfInstanceDefinition)
inst.BinaryReadFrom(r)
name, _ := readUTF16StringAtPos(r, instOffset+int64(inst.NameOffset), inst.NameLength)
pos := instOffset + int64(inst.ByteLength)
offset, counters := parseCounterBlock(buffer, r, pos, counterDefs)
instances[i] = &PerfInstance{
Name: name,
Counters: counters,
rawData: inst,
}
instOffset = pos + offset
}
}
// Next perfObjectType
objOffset += int64(obj.TotalByteLength)
}
return objects, nil
}
func parseCounterBlock(b []byte, r io.ReadSeeker, pos int64, defs []*PerfCounterDef) (int64, []*PerfCounter) {
r.Seek(pos, io.SeekStart)
block := new(perfCounterBlock)
block.BinaryReadFrom(r)
counters := make([]*PerfCounter, len(defs))
for i, def := range defs {
valueOffset := pos + int64(def.rawData.CounterOffset)
value := convertCounterValue(def.rawData, b, valueOffset)
counters[i] = &PerfCounter{
Value: value,
Def: def,
}
}
return int64(block.ByteLength), counters
}
func convertCounterValue(counterDef *perfCounterDefinition, buffer []byte, valueOffset int64) (value 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:
value = int64(bo.Uint32(buffer[valueOffset:(valueOffset + 4)]))
case 8:
value = int64(bo.Uint64(buffer[valueOffset:(valueOffset + 8)]))
default:
value = int64(bo.Uint32(buffer[valueOffset:(valueOffset + 4)]))
}
return
}
// Sort slice of objects by index. This is useful for displaying
// a human-readable list or dump, but unnecessary otherwise.
func SortObjects(p []*PerfObject) {
sort.Slice(p, func(i, j int) bool {
return p[i].NameIndex < p[j].NameIndex
})
}

View File

@@ -0,0 +1,180 @@
package perflib
import (
"encoding/binary"
"io"
"syscall"
)
type binaryReaderFrom interface {
BinaryReadFrom(r io.Reader) error
}
/*
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 syscall.Systemtime
_ uint32 // TODO
PerfTime int64
PerfFreq int64
PerfTime100nSec int64
SystemNameLength uint32
SystemNameOffset uint32
}
func (p *perfDataBlock) BinaryReadFrom(r io.Reader) error {
return binary.Read(r, bo, p)
}
/*
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
}
func (p *perfObjectType) BinaryReadFrom(r io.Reader) error {
return binary.Read(r, bo, p)
}
/*
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
}
func (p *perfCounterDefinition) BinaryReadFrom(r io.Reader) error {
return binary.Read(r, bo, p)
}
func (p *perfCounterDefinition) LookupName() string {
return counterNameTable.LookupString(p.CounterNameTitleIndex)
}
func (p *perfCounterDefinition) LookupHelp() string {
return helpNameTable.LookupString(p.CounterHelpTitleIndex)
}
/*
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
}
func (p *perfCounterBlock) BinaryReadFrom(r io.Reader) error {
return binary.Read(r, bo, p)
}
/*
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
}
func (p *perfInstanceDefinition) BinaryReadFrom(r io.Reader) error {
return binary.Read(r, bo, p)
}

View File

@@ -0,0 +1,49 @@
package perflib
import (
"encoding/binary"
"io"
"syscall"
)
// Read an unterminated UTF16 string at a given position, specifying its length
func readUTF16StringAtPos(r io.ReadSeeker, absPos int64, length uint32) (string, error) {
value := make([]uint16, length/2)
_, err := r.Seek(absPos, io.SeekStart)
if err != nil {
return "", err
}
err = binary.Read(r, bo, value)
if err != nil {
return "", err
}
return syscall.UTF16ToString(value), nil
}
// Reads a null-terminated UTF16 string at the current offset
func readUTF16String(r io.Reader) (string, error) {
var err error
b := make([]byte, 2)
out := make([]uint16, 0, 100)
for i := 0; err == nil; i += 2 {
_, err = r.Read(b)
if b[0] == 0 && b[1] == 0 {
break
}
out = append(out, bo.Uint16(b))
}
if err != nil {
return "", err
}
return syscall.UTF16ToString(out), nil
}