From 24e6d6d35039a55835e3a0f661fda21f90b7e19e Mon Sep 17 00:00:00 2001 From: AtakanColak Date: Thu, 22 Oct 2020 17:13:43 +0300 Subject: [PATCH 1/6] add load to windows --- internal/common/common_windows.go | 66 +++++++++++++++++++++++++++---- load/load_test.go | 17 ++++++++ load/load_windows.go | 61 +++++++++++++++++++++++++++- 3 files changed, 135 insertions(+), 9 deletions(-) diff --git a/internal/common/common_windows.go b/internal/common/common_windows.go index 9bc05de..0064b9e 100644 --- a/internal/common/common_windows.go +++ b/internal/common/common_windows.go @@ -4,6 +4,7 @@ package common import ( "context" + "fmt" "path/filepath" "strings" "syscall" @@ -69,13 +70,13 @@ var ( ProcNtWow64QueryInformationProcess64 = ModNt.NewProc("NtWow64QueryInformationProcess64") ProcNtWow64ReadVirtualMemory64 = ModNt.NewProc("NtWow64ReadVirtualMemory64") - PdhOpenQuery = ModPdh.NewProc("PdhOpenQuery") - PdhAddCounter = ModPdh.NewProc("PdhAddCounterW") - PdhCollectQueryData = ModPdh.NewProc("PdhCollectQueryData") - PdhGetFormattedCounterValue = ModPdh.NewProc("PdhGetFormattedCounterValue") - PdhCloseQuery = ModPdh.NewProc("PdhCloseQuery") + PdhOpenQuery = ModPdh.NewProc("PdhOpenQuery") + PdhAddCounter = ModPdh.NewProc("PdhAddCounterW") + PdhCollectQueryData = ModPdh.NewProc("PdhCollectQueryData") + PdhGetFormattedCounterValue = ModPdh.NewProc("PdhGetFormattedCounterValue") + PdhCloseQuery = ModPdh.NewProc("PdhCloseQuery") - procQueryDosDeviceW = Modkernel32.NewProc("QueryDosDeviceW") + procQueryDosDeviceW = Modkernel32.NewProc("QueryDosDeviceW") ) type FILETIME struct { @@ -93,7 +94,7 @@ func BytePtrToString(p *uint8) string { return string(a[:i]) } -// CounterInfo +// CounterInfo XXX // copied from https://github.com/mackerelio/mackerel-agent/ type CounterInfo struct { PostName string @@ -130,6 +131,57 @@ func CreateCounter(query windows.Handle, pname, cname string) (*CounterInfo, err }, nil } +// GetCounterValue get counter value from handle +// adapted from https://github.com/mackerelio/mackerel-agent/ +func GetCounterValue(counter windows.Handle) (float64, error) { + var value PDH_FMT_COUNTERVALUE_DOUBLE + r, _, err := PdhGetFormattedCounterValue.Call(uintptr(counter), PDH_FMT_DOUBLE, uintptr(0), uintptr(unsafe.Pointer(&value))) + if r != 0 && r != PDH_INVALID_DATA { + return 0.0, err + } + return value.DoubleValue, nil +} + +// ProcessorQueueLengthGenerator is struct of windows api +// adapted from https://github.com/mackerelio/mackerel-agent/ +type ProcessorQueueLengthGenerator struct { + query windows.Handle + counter *CounterInfo +} + +// NewProcessorQueueLengthGenerator is set up windows api +// adapted from https://github.com/mackerelio/mackerel-agent/ +func NewProcessorQueueLengthGenerator() (*ProcessorQueueLengthGenerator, error) { + g := &ProcessorQueueLengthGenerator{0, nil} + + var err error + g.query, err = CreateQuery() + if err != nil { + return nil, err + } + + counter, err := CreateCounter(g.query, "processor_queue_length", `\System\Processor Queue Length`) + if err != nil { + return nil, err + } + g.counter = counter + return g, nil +} + +// Generate XXX +// adapted from https://github.com/mackerelio/mackerel-agent/ +func (g *ProcessorQueueLengthGenerator) Generate() (float64, error) { + r, _, err := PdhCollectQueryData.Call(uintptr(g.query)) + if r != 0 && err != nil { + if r == PDH_NO_DATA { + return 0.0, fmt.Errorf("%w: this metric has not data", err) + } + return 0.0, err + } + + return GetCounterValue(g.counter.Counter) +} + // WMIQueryWithContext - wraps wmi.Query with a timed-out context to avoid hanging func WMIQueryWithContext(ctx context.Context, query string, dst interface{}, connectServerArgs ...interface{}) error { if _, ok := ctx.Deadline(); !ok { diff --git a/load/load_test.go b/load/load_test.go index 9c8735d..ffa7887 100644 --- a/load/load_test.go +++ b/load/load_test.go @@ -27,6 +27,23 @@ func TestLoad(t *testing.T) { t.Log(v) } +// Commented out to not to slow down CI +// Do conduct heavy cpu load on he computer to observe change +// func TestLoadWithInterval(t *testing.T) { +// interval := 5 +// iteration := 110 / interval + +// for i := 0; i < iteration; i++ { +// v, err := Avg() +// skipIfNotImplementedErr(t, err) +// if err != nil { +// t.Errorf("error %v", err) +// } +// t.Log(v) +// time.Sleep(time.Duration(interval) * time.Second) +// } +// } + func TestLoadAvgStat_String(t *testing.T) { v := AvgStat{ Load1: 10.1, diff --git a/load/load_windows.go b/load/load_windows.go index 42968b3..fc70ece 100644 --- a/load/load_windows.go +++ b/load/load_windows.go @@ -4,18 +4,75 @@ package load import ( "context" + "sync" + "time" "github.com/shirou/gopsutil/internal/common" ) +var ( + loadErr error + loadAvg1M float64 = 0.0 + loadAvg5M float64 = 0.0 + loadAvg15M float64 = 0.0 + loadAvgMutex sync.RWMutex + loadAvgGoroutineOnce sync.Once +) + +// loadAvgGoroutine updates avg data by fetching current load by interval +// TODO register callback rather than this +// see https://psutil.readthedocs.io/en/latest/#psutil.getloadavg +// code https://github.com/giampaolo/psutil/blob/master/psutil/arch/windows/wmi.c +func loadAvgGoroutine() { + const ( + loadAvgFactor1F = 0.9200444146293232478931553241 + loadAvgFactor5F = 0.9834714538216174894737477501 + loadAvgFactor15F = 0.9944598480048967508795473394 + ) + + var ( + interval = 5 * time.Second + ) + + generator, err := common.NewProcessorQueueLengthGenerator() + if err != nil { + loadAvgMutex.Lock() + loadErr = err + loadAvgMutex.Unlock() + return + } + for { + time.Sleep(interval) + if generator == nil { + return + } + currentLoad, err := generator.Generate() + loadAvgMutex.Lock() + loadErr = err + if err == nil { + loadAvg1M = loadAvg1M*loadAvgFactor1F + currentLoad*(1.0-loadAvgFactor1F) + loadAvg5M = loadAvg5M*loadAvgFactor5F + currentLoad*(1.0-loadAvgFactor5F) + loadAvg15M = loadAvg15M*loadAvgFactor15F + currentLoad*(1.0-loadAvgFactor15F) + } + loadAvgMutex.Unlock() + } +} + func Avg() (*AvgStat, error) { return AvgWithContext(context.Background()) } func AvgWithContext(ctx context.Context) (*AvgStat, error) { - ret := AvgStat{} + loadAvgGoroutineOnce.Do(func() { go loadAvgGoroutine() }) + loadAvgMutex.RLock() + defer loadAvgMutex.RUnlock() + ret := AvgStat{ + Load1: loadAvg1M, + Load5: loadAvg5M, + Load15: loadAvg15M, + } - return &ret, common.ErrNotImplementedError + return &ret, loadErr } func Misc() (*MiscStat, error) { From 2c98f69e113a4e72ce523dd4ac7e64f55a62c708 Mon Sep 17 00:00:00 2001 From: AtakanColak Date: Mon, 2 Nov 2020 16:51:13 +0300 Subject: [PATCH 2/6] add load v2 tidied up common/windows added load benchmark for load_test cpu percent is used as load if queue is zero --- internal/common/common_windows.go | 51 +++++++++++++++------------ load/load_test.go | 44 +++++++++++++---------- load/load_windows.go | 58 ++++++++++++++++++++++--------- 3 files changed, 96 insertions(+), 57 deletions(-) diff --git a/internal/common/common_windows.go b/internal/common/common_windows.go index 0064b9e..0a7aff9 100644 --- a/internal/common/common_windows.go +++ b/internal/common/common_windows.go @@ -142,44 +142,49 @@ func GetCounterValue(counter windows.Handle) (float64, error) { return value.DoubleValue, nil } -// ProcessorQueueLengthGenerator is struct of windows api -// adapted from https://github.com/mackerelio/mackerel-agent/ -type ProcessorQueueLengthGenerator struct { - query windows.Handle - counter *CounterInfo +type Win32PerformanceCounter struct { + PostName string + CounterName string + Query windows.Handle + Counter windows.Handle } -// NewProcessorQueueLengthGenerator is set up windows api -// adapted from https://github.com/mackerelio/mackerel-agent/ -func NewProcessorQueueLengthGenerator() (*ProcessorQueueLengthGenerator, error) { - g := &ProcessorQueueLengthGenerator{0, nil} - - var err error - g.query, err = CreateQuery() +func NewWin32PerformanceCounter(postName, counterName string) (*Win32PerformanceCounter, error) { + query, err := CreateQuery() if err != nil { return nil, err } - - counter, err := CreateCounter(g.query, "processor_queue_length", `\System\Processor Queue Length`) - if err != nil { + var counter = Win32PerformanceCounter{ + Query: query, + PostName: postName, + CounterName: counterName, + } + r, _, err := PdhAddCounter.Call( + uintptr(counter.Query), + uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(counter.CounterName))), + 0, + uintptr(unsafe.Pointer(&counter.Counter)), + ) + if r != 0 { return nil, err } - g.counter = counter - return g, nil + return &counter, nil } -// Generate XXX -// adapted from https://github.com/mackerelio/mackerel-agent/ -func (g *ProcessorQueueLengthGenerator) Generate() (float64, error) { - r, _, err := PdhCollectQueryData.Call(uintptr(g.query)) +func (w *Win32PerformanceCounter) GetValue() (float64, error) { + r, _, err := PdhCollectQueryData.Call(uintptr(w.Query)) if r != 0 && err != nil { if r == PDH_NO_DATA { - return 0.0, fmt.Errorf("%w: this metric has not data", err) + return 0.0, fmt.Errorf("%w: this counter has not data", err) } return 0.0, err } - return GetCounterValue(g.counter.Counter) + return GetCounterValue(w.Counter) +} + +func ProcessorQueueLengthCounter() (*Win32PerformanceCounter, error) { + return NewWin32PerformanceCounter("processor_queue_length", `\System\Processor Queue Length`) } // WMIQueryWithContext - wraps wmi.Query with a timed-out context to avoid hanging diff --git a/load/load_test.go b/load/load_test.go index ffa7887..72f1fc9 100644 --- a/load/load_test.go +++ b/load/load_test.go @@ -7,7 +7,7 @@ import ( "github.com/shirou/gopsutil/internal/common" ) -func skipIfNotImplementedErr(t *testing.T, err error) { +func skipIfNotImplementedErr(t testing.TB, err error) { if err == common.ErrNotImplementedError { t.Skip("not implemented") } @@ -27,23 +27,6 @@ func TestLoad(t *testing.T) { t.Log(v) } -// Commented out to not to slow down CI -// Do conduct heavy cpu load on he computer to observe change -// func TestLoadWithInterval(t *testing.T) { -// interval := 5 -// iteration := 110 / interval - -// for i := 0; i < iteration; i++ { -// v, err := Avg() -// skipIfNotImplementedErr(t, err) -// if err != nil { -// t.Errorf("error %v", err) -// } -// t.Log(v) -// time.Sleep(time.Duration(interval) * time.Second) -// } -// } - func TestLoadAvgStat_String(t *testing.T) { v := AvgStat{ Load1: 10.1, @@ -84,3 +67,28 @@ func TestMiscStatString(t *testing.T) { } t.Log(e) } + +func BenchmarkLoad(b *testing.B) { + + loadAvg := func(t testing.TB) { + v, err := Avg() + skipIfNotImplementedErr(t, err) + if err != nil { + t.Errorf("error %v", err) + } + empty := &AvgStat{} + if v == empty { + t.Errorf("error load: %v", v) + } + } + + b.Run("FirstCall", func(b *testing.B) { + loadAvg(b) + }) + + b.Run("SubsequentCalls", func(b *testing.B) { + for i := 0; i < b.N; i++ { + loadAvg(b) + } + }) +} diff --git a/load/load_windows.go b/load/load_windows.go index fc70ece..7ab6200 100644 --- a/load/load_windows.go +++ b/load/load_windows.go @@ -4,9 +4,11 @@ package load import ( "context" + "log" "sync" "time" + "github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/internal/common" ) @@ -31,30 +33,51 @@ func loadAvgGoroutine() { ) var ( - interval = 5 * time.Second + sleepInterval time.Duration = 5 * time.Second + currentLoad float64 ) - generator, err := common.NewProcessorQueueLengthGenerator() - if err != nil { - loadAvgMutex.Lock() - loadErr = err + counter, err := common.ProcessorQueueLengthCounter() + loadErr = err + if err != nil || counter == nil { loadAvgMutex.Unlock() + log.Println("unexpected processor queue length counter error, please file an issue on github") return } + for { - time.Sleep(interval) - if generator == nil { - return + currentLoad, loadErr = counter.GetValue() + if loadErr != nil { + goto SKIP } - currentLoad, err := generator.Generate() - loadAvgMutex.Lock() - loadErr = err - if err == nil { - loadAvg1M = loadAvg1M*loadAvgFactor1F + currentLoad*(1.0-loadAvgFactor1F) - loadAvg5M = loadAvg5M*loadAvgFactor5F + currentLoad*(1.0-loadAvgFactor5F) - loadAvg15M = loadAvg15M*loadAvgFactor15F + currentLoad*(1.0-loadAvgFactor15F) + // comment following block if you want load to be 0 as long as process queue is zero + { + if currentLoad == 0.0 { + percent, err := cpu.Percent(0, false) + if err == nil { + currentLoad = percent[0] / 100 + // load averages are also given some amount of the currentLoad + // maybe they shouldnt? + if loadAvg1M == 0 { + loadAvg1M = currentLoad + } + if loadAvg5M == 0 { + loadAvg5M = currentLoad / 2 + } + if loadAvg15M == 0 { + loadAvg15M = currentLoad / 3 + } + } + } } + loadAvg1M = loadAvg1M*loadAvgFactor1F + currentLoad*(1.0-loadAvgFactor1F) + loadAvg5M = loadAvg5M*loadAvgFactor5F + currentLoad*(1.0-loadAvgFactor5F) + loadAvg15M = loadAvg15M*loadAvgFactor15F + currentLoad*(1.0-loadAvgFactor15F) + + SKIP: loadAvgMutex.Unlock() + time.Sleep(sleepInterval) + loadAvgMutex.Lock() } } @@ -63,7 +86,10 @@ func Avg() (*AvgStat, error) { } func AvgWithContext(ctx context.Context) (*AvgStat, error) { - loadAvgGoroutineOnce.Do(func() { go loadAvgGoroutine() }) + loadAvgGoroutineOnce.Do(func() { + loadAvgMutex.Lock() + go loadAvgGoroutine() + }) loadAvgMutex.RLock() defer loadAvgMutex.RUnlock() ret := AvgStat{ From 498a39984d9d3200411e881fd56dcb4ef09e166d Mon Sep 17 00:00:00 2001 From: AtakanColak Date: Fri, 6 Nov 2020 11:07:02 +0300 Subject: [PATCH 3/6] tidy comments --- internal/common/common_windows.go | 6 +++--- load/load_windows.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/common/common_windows.go b/internal/common/common_windows.go index 0a7aff9..cbf4f06 100644 --- a/internal/common/common_windows.go +++ b/internal/common/common_windows.go @@ -94,7 +94,7 @@ func BytePtrToString(p *uint8) string { return string(a[:i]) } -// CounterInfo XXX +// CounterInfo struct is used to track a windows performance counter // copied from https://github.com/mackerelio/mackerel-agent/ type CounterInfo struct { PostName string @@ -102,7 +102,7 @@ type CounterInfo struct { Counter windows.Handle } -// CreateQuery XXX +// CreateQuery with a PdhOpenQuery call // copied from https://github.com/mackerelio/mackerel-agent/ func CreateQuery() (windows.Handle, error) { var query windows.Handle @@ -113,7 +113,7 @@ func CreateQuery() (windows.Handle, error) { return query, nil } -// CreateCounter XXX +// CreateCounter with a PdhAddCounter call func CreateCounter(query windows.Handle, pname, cname string) (*CounterInfo, error) { var counter windows.Handle r, _, err := PdhAddCounter.Call( diff --git a/load/load_windows.go b/load/load_windows.go index 7ab6200..381f61e 100644 --- a/load/load_windows.go +++ b/load/load_windows.go @@ -22,9 +22,9 @@ var ( ) // loadAvgGoroutine updates avg data by fetching current load by interval -// TODO register callback rather than this +// TODO instead of this goroutine, we can register a Win32 counter just as psutil does // see https://psutil.readthedocs.io/en/latest/#psutil.getloadavg -// code https://github.com/giampaolo/psutil/blob/master/psutil/arch/windows/wmi.c +// code https://github.com/giampaolo/psutil/blob/8415355c8badc9c94418b19bdf26e622f06f0cce/psutil/arch/windows/wmi.c func loadAvgGoroutine() { const ( loadAvgFactor1F = 0.9200444146293232478931553241 From fb44aead395c142498d1896026493c89064fbc08 Mon Sep 17 00:00:00 2001 From: AtakanColak Date: Mon, 9 Nov 2020 10:00:22 +0300 Subject: [PATCH 4/6] use ticker and explicit contants --- load/load_windows.go | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/load/load_windows.go b/load/load_windows.go index 381f61e..4cbe19a 100644 --- a/load/load_windows.go +++ b/load/load_windows.go @@ -5,6 +5,7 @@ package load import ( "context" "log" + "math" "sync" "time" @@ -26,15 +27,12 @@ var ( // see https://psutil.readthedocs.io/en/latest/#psutil.getloadavg // code https://github.com/giampaolo/psutil/blob/8415355c8badc9c94418b19bdf26e622f06f0cce/psutil/arch/windows/wmi.c func loadAvgGoroutine() { - const ( - loadAvgFactor1F = 0.9200444146293232478931553241 - loadAvgFactor5F = 0.9834714538216174894737477501 - loadAvgFactor15F = 0.9944598480048967508795473394 - ) - var ( - sleepInterval time.Duration = 5 * time.Second - currentLoad float64 + samplingFrequency time.Duration = 5 * time.Second + loadAvgFactor1M float64 = 1 / math.Exp(samplingFrequency.Seconds()/time.Minute.Seconds()) + loadAvgFactor5M float64 = 1 / math.Exp(samplingFrequency.Seconds()/(5*time.Minute).Seconds()) + loadAvgFactor15M float64 = 1 / math.Exp(samplingFrequency.Seconds()/(15*time.Minute).Seconds()) + currentLoad float64 ) counter, err := common.ProcessorQueueLengthCounter() @@ -45,6 +43,7 @@ func loadAvgGoroutine() { return } + tick := time.NewTicker(samplingFrequency).C for { currentLoad, loadErr = counter.GetValue() if loadErr != nil { @@ -70,13 +69,13 @@ func loadAvgGoroutine() { } } } - loadAvg1M = loadAvg1M*loadAvgFactor1F + currentLoad*(1.0-loadAvgFactor1F) - loadAvg5M = loadAvg5M*loadAvgFactor5F + currentLoad*(1.0-loadAvgFactor5F) - loadAvg15M = loadAvg15M*loadAvgFactor15F + currentLoad*(1.0-loadAvgFactor15F) + loadAvg1M = loadAvg1M*loadAvgFactor1M + currentLoad*(1-loadAvgFactor1M) + loadAvg5M = loadAvg5M*loadAvgFactor5M + currentLoad*(1-loadAvgFactor5M) + loadAvg15M = loadAvg15M*loadAvgFactor15M + currentLoad*(1-loadAvgFactor15M) SKIP: loadAvgMutex.Unlock() - time.Sleep(sleepInterval) + <-tick loadAvgMutex.Lock() } } From b2a35742162c3a4698ff2cd1090860bebe68c4aa Mon Sep 17 00:00:00 2001 From: AtakanColak Date: Mon, 9 Nov 2020 10:37:31 +0300 Subject: [PATCH 5/6] remove percent --- load/load_windows.go | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/load/load_windows.go b/load/load_windows.go index 4cbe19a..73f7926 100644 --- a/load/load_windows.go +++ b/load/load_windows.go @@ -9,7 +9,6 @@ import ( "sync" "time" - "github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/internal/common" ) @@ -49,26 +48,7 @@ func loadAvgGoroutine() { if loadErr != nil { goto SKIP } - // comment following block if you want load to be 0 as long as process queue is zero - { - if currentLoad == 0.0 { - percent, err := cpu.Percent(0, false) - if err == nil { - currentLoad = percent[0] / 100 - // load averages are also given some amount of the currentLoad - // maybe they shouldnt? - if loadAvg1M == 0 { - loadAvg1M = currentLoad - } - if loadAvg5M == 0 { - loadAvg5M = currentLoad / 2 - } - if loadAvg15M == 0 { - loadAvg15M = currentLoad / 3 - } - } - } - } + loadAvg1M = loadAvg1M*loadAvgFactor1M + currentLoad*(1-loadAvgFactor1M) loadAvg5M = loadAvg5M*loadAvgFactor5M + currentLoad*(1-loadAvgFactor5M) loadAvg15M = loadAvg15M*loadAvgFactor15M + currentLoad*(1-loadAvgFactor15M) @@ -80,13 +60,14 @@ func loadAvgGoroutine() { } } +// Avg for Windows may return 0 values for the first few 5 second intervals func Avg() (*AvgStat, error) { return AvgWithContext(context.Background()) } func AvgWithContext(ctx context.Context) (*AvgStat, error) { loadAvgGoroutineOnce.Do(func() { - loadAvgMutex.Lock() + loadAvgMutex.Lock() // lock to be unlocked by loadAvgGoroutine go loadAvgGoroutine() }) loadAvgMutex.RLock() From 277c95057bc2bf9327a0dcfe910c2382d59eeef7 Mon Sep 17 00:00:00 2001 From: AtakanColak Date: Mon, 9 Nov 2020 10:50:09 +0300 Subject: [PATCH 6/6] tidy up loop control and mutex --- load/load_windows.go | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/load/load_windows.go b/load/load_windows.go index 73f7926..5f3c695 100644 --- a/load/load_windows.go +++ b/load/load_windows.go @@ -35,28 +35,21 @@ func loadAvgGoroutine() { ) counter, err := common.ProcessorQueueLengthCounter() - loadErr = err if err != nil || counter == nil { - loadAvgMutex.Unlock() - log.Println("unexpected processor queue length counter error, please file an issue on github") + log.Println("gopsutil: unexpected processor queue length counter error, please file an issue on github") return } tick := time.NewTicker(samplingFrequency).C for { - currentLoad, loadErr = counter.GetValue() - if loadErr != nil { - goto SKIP - } - + currentLoad, err = counter.GetValue() + loadAvgMutex.Lock() + loadErr = err loadAvg1M = loadAvg1M*loadAvgFactor1M + currentLoad*(1-loadAvgFactor1M) loadAvg5M = loadAvg5M*loadAvgFactor5M + currentLoad*(1-loadAvgFactor5M) loadAvg15M = loadAvg15M*loadAvgFactor15M + currentLoad*(1-loadAvgFactor15M) - - SKIP: loadAvgMutex.Unlock() <-tick - loadAvgMutex.Lock() } } @@ -67,7 +60,6 @@ func Avg() (*AvgStat, error) { func AvgWithContext(ctx context.Context) (*AvgStat, error) { loadAvgGoroutineOnce.Do(func() { - loadAvgMutex.Lock() // lock to be unlocked by loadAvgGoroutine go loadAvgGoroutine() }) loadAvgMutex.RLock()