From 4ef0ddafc719ecb041e453c8cbb5338cfb279a0e Mon Sep 17 00:00:00 2001 From: nikita-vanyasin Date: Thu, 21 Feb 2019 23:26:14 +0300 Subject: [PATCH 1/2] Eliminate WMI queries when calling cpu.Times with percpu=True based on https://github.com/signalfx/signalfx-agent/blob/e89f2c5fad499a2637010ac32a48b16a0d7b4173/internal/monitors/cpu/cpu_windows.go --- cpu/cpu_windows.go | 100 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 65 insertions(+), 35 deletions(-) diff --git a/cpu/cpu_windows.go b/cpu/cpu_windows.go index 3959212..22ebbd5 100644 --- a/cpu/cpu_windows.go +++ b/cpu/cpu_windows.go @@ -23,18 +23,18 @@ type Win32_Processor struct { MaxClockSpeed uint32 } -type win32_PerfRawData_Counters_ProcessorInformation struct { - Name string - PercentDPCTime uint64 - PercentIdleTime uint64 - PercentUserTime uint64 - PercentProcessorTime uint64 - PercentInterruptTime uint64 - PercentPriorityTime uint64 - PercentPrivilegedTime uint64 - InterruptsPerSec uint32 - ProcessorFrequency uint32 - DPCRate uint32 +// SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION +// defined in windows api doc with the following +// https://docs.microsoft.com/en-us/windows/desktop/api/winternl/nf-winternl-ntquerysysteminformation#system_processor_performance_information +// additional fields documented here +// https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/processor_performance.htm +type win32_SystemProcessorPerformanceInformation struct { + IdleTime int64 // idle time in 100ns (this is not a filetime). + KernelTime int64 // kernel time in 100ns. kernel time includes idle time. (this is not a filetime). + UserTime int64 // usertime in 100ns (this is not a filetime). + DpcTime int64 // dpc time in 100ns (this is not a filetime). + InterruptTime int64 // interrupt time in 100ns + InterruptCount uint32 } // Win32_PerfFormattedData_PerfOS_System struct to have count of processes and processor queue length @@ -45,6 +45,13 @@ type Win32_PerfFormattedData_PerfOS_System struct { const ( win32_TicksPerSecond = 10000000.0 + + // systemProcessorPerformanceInformationClass information class to query with NTQuerySystemInformation + // https://processhacker.sourceforge.io/doc/ntexapi_8h.html#ad5d815b48e8f4da1ef2eb7a2f18a54e0 + win32_SystemProcessorPerformanceInformationClass = 8 + + // size of systemProcessorPerformanceInfoSize in memory + win32_SystemProcessorPerformanceInfoSize = uint32(unsafe.Sizeof(win32_SystemProcessorPerformanceInformation{})) ) // Times returns times stat per cpu and combined for all CPUs @@ -54,7 +61,7 @@ func Times(percpu bool) ([]TimesStat, error) { func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) { if percpu { - return perCPUTimesWithContext(ctx) + return perCPUTimesWithContext() } var ret []TimesStat @@ -120,20 +127,6 @@ func InfoWithContext(ctx context.Context) ([]InfoStat, error) { return ret, nil } -// PerfInfo returns the performance counter's instance value for ProcessorInformation. -// Name property is the key by which overall, per cpu and per core metric is known. -func perfInfoWithContext(ctx context.Context) ([]win32_PerfRawData_Counters_ProcessorInformation, error) { - var ret []win32_PerfRawData_Counters_ProcessorInformation - - q := wmi.CreateQuery(&ret, "WHERE NOT Name LIKE '%_Total'") - err := common.WMIQueryWithContext(ctx, q, &ret) - if err != nil { - return []win32_PerfRawData_Counters_ProcessorInformation{}, err - } - - return ret, err -} - // ProcInfo returns processes count and processor queue length in the system. // There is a single queue for processor even on multiprocessors systems. func ProcInfo() ([]Win32_PerfFormattedData_PerfOS_System, error) { @@ -151,21 +144,58 @@ func ProcInfoWithContext(ctx context.Context) ([]Win32_PerfFormattedData_PerfOS_ } // perCPUTimes returns times stat per cpu, per core and overall for all CPUs -func perCPUTimesWithContext(ctx context.Context) ([]TimesStat, error) { +func perCPUTimesWithContext() ([]TimesStat, error) { var ret []TimesStat - stats, err := perfInfoWithContext(ctx) + stats, err := perfInfoWithContext() if err != nil { return nil, err } - for _, v := range stats { + for core, v := range stats { c := TimesStat{ - CPU: v.Name, - User: float64(v.PercentUserTime) / win32_TicksPerSecond, - System: float64(v.PercentPrivilegedTime) / win32_TicksPerSecond, - Idle: float64(v.PercentIdleTime) / win32_TicksPerSecond, - Irq: float64(v.PercentInterruptTime) / win32_TicksPerSecond, + CPU: fmt.Sprintf("cpu%d", core), + User: float64(v.UserTime) / win32_TicksPerSecond, + System: float64(v.KernelTime-v.IdleTime) / win32_TicksPerSecond, + Idle: float64(v.IdleTime) / win32_TicksPerSecond, + Irq: float64(v.InterruptTime) / win32_TicksPerSecond, } ret = append(ret, c) } return ret, nil } + +// makes call to Windows API function to retrieve performance information for each core +func perfInfoWithContext() ([]win32_SystemProcessorPerformanceInformation, error) { + // Make maxResults large for safety. + // We can't invoke the api call with a results array that's too small. + // If we have more than 2056 cores on a single host, then it's probably the future. + maxBuffer := 2056 + // buffer for results from the windows proc + resultBuffer := make([]win32_SystemProcessorPerformanceInformation, maxBuffer) + // size of the buffer in memory + bufferSize := uintptr(win32_SystemProcessorPerformanceInfoSize) * uintptr(maxBuffer) + // size of the returned response + var retSize uint32 + + // Invoke windows api proc. + // The returned err from the windows dll proc will always be non-nil even when successful. + // See https://godoc.org/golang.org/x/sys/windows#LazyProc.Call for more information + retCode, _, err := common.ProcNtQuerySystemInformation.Call( + win32_SystemProcessorPerformanceInformationClass, // System Information Class -> SystemProcessorPerformanceInformation + uintptr(unsafe.Pointer(&resultBuffer[0])), // pointer to first element in result buffer + bufferSize, // size of the buffer in memory + uintptr(unsafe.Pointer(&retSize)), // pointer to the size of the returned results the windows proc will set this + ) + + // check return code for errors + if retCode != 0 { + return nil, fmt.Errorf("call to NtQuerySystemInformation returned %d. err: %s", retCode, err.Error()) + } + + // calculate the number of returned elements based on the returned size + numReturnedElements := retSize / win32_SystemProcessorPerformanceInfoSize + + // trim results to the number of returned elements + resultBuffer = resultBuffer[:numReturnedElements] + + return resultBuffer, nil +} From 59b002e5c296fc8570f401627c1d6c8936895b45 Mon Sep 17 00:00:00 2001 From: nikita-vanyasin Date: Fri, 22 Feb 2019 11:19:56 +0300 Subject: [PATCH 2/2] Fix function naming --- cpu/cpu_windows.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cpu/cpu_windows.go b/cpu/cpu_windows.go index 22ebbd5..4755913 100644 --- a/cpu/cpu_windows.go +++ b/cpu/cpu_windows.go @@ -61,7 +61,7 @@ func Times(percpu bool) ([]TimesStat, error) { func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) { if percpu { - return perCPUTimesWithContext() + return perCPUTimes() } var ret []TimesStat @@ -144,9 +144,9 @@ func ProcInfoWithContext(ctx context.Context) ([]Win32_PerfFormattedData_PerfOS_ } // perCPUTimes returns times stat per cpu, per core and overall for all CPUs -func perCPUTimesWithContext() ([]TimesStat, error) { +func perCPUTimes() ([]TimesStat, error) { var ret []TimesStat - stats, err := perfInfoWithContext() + stats, err := perfInfo() if err != nil { return nil, err } @@ -164,7 +164,7 @@ func perCPUTimesWithContext() ([]TimesStat, error) { } // makes call to Windows API function to retrieve performance information for each core -func perfInfoWithContext() ([]win32_SystemProcessorPerformanceInformation, error) { +func perfInfo() ([]win32_SystemProcessorPerformanceInformation, error) { // Make maxResults large for safety. // We can't invoke the api call with a results array that's too small. // If we have more than 2056 cores on a single host, then it's probably the future.