Skip to content

Commit

Permalink
ROCANA-10331: ProcTime reports CPU usage percent (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamlamar authored May 17, 2017
1 parent 22f076e commit ca14ae9
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 29 deletions.
4 changes: 4 additions & 0 deletions sigar_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ func (self *ProcTime) Get(pid int) error {
return nil
}

func (self *ProcTime) CalculateCpuPercent(other *ProcTime) error {
return notImplemented()
}

func (self *ProcArgs) Get(pid int) error {
var args []string

Expand Down
27 changes: 17 additions & 10 deletions sigar_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,19 +353,26 @@ type ProcIo struct {
}

type ProcMem struct {
Size uint64
Resident uint64
Share uint64
MinorFaults uint64
MajorFaults uint64
PageFaults uint64
Size uint64
Resident uint64
Share uint64
MinorFaults uint64
MajorFaults uint64
PageFaults uint64
PageFileBytes uint64 // Currently only collected on Windows
}

type ProcTime struct {
StartTime uint64
User uint64
Sys uint64
Total uint64
CollectionTime time.Time
StartTime uint64 // Milliseconds since epoch
User uint64 // User time in milliseconds
Sys uint64 // System time in milliseconds
Total uint64 // Total time in milliseconds

// Not valid until after CalculateCpuPercent()
PercentUserTime uint64
PercentSysTime uint64
PercentTotalTime uint64
}

type ProcArgs struct {
Expand Down
52 changes: 47 additions & 5 deletions sigar_interface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,27 @@ import (
"os"
"path/filepath"
"runtime"
"time"

. "github.com/scalingdata/ginkgo"
. "github.com/scalingdata/gomega"

. "github.com/scalingdata/gosigar"
)

func burnOneSecondCpu() int {
timer := time.NewTimer(time.Second)
var i int
for {
select {
case <-timer.C:
return i
default:
i++
}
}
}

var _ = Describe("Sigar", func() {
var invalidPid = 666666

Expand Down Expand Up @@ -211,13 +225,41 @@ var _ = Describe("Sigar", func() {
})

It("proc time", func() {
time := ProcTime{}
err := time.Get(os.Getppid())
// Measure parent process
timeParent := ProcTime{}
err := timeParent.Get(os.Getppid())
Expect(err).ToNot(HaveOccurred())
Expect(timeParent.User).To(BeNumerically(">", 0))
Expect(timeParent.Sys).To(BeNumerically(">", 0))

// Take snapshot of current process
time1 := ProcTime{}
err = time1.Get(os.Getpid())
Expect(err).ToNot(HaveOccurred())

// Use some CPU time
i := burnOneSecondCpu()
Expect(i).To(BeNumerically(">", 0)) // Assert on i to avoid compiler optimization

time2 := ProcTime{}
err = time2.Get(os.Getpid())
Expect(err).ToNot(HaveOccurred())
Expect(time.User).To(BeNumerically(">", 0))
Expect(time.Sys).To(BeNumerically(">", 0))
Expect(time2.User).To(BeNumerically(">", 0))

err = time2.CalculateCpuPercent(&time1)
if runtime.GOOS == "darwin" {
Expect(err).To(Equal(ErrNotImplemented))
} else if runtime.GOOS == "windows" {
// Counters are not reliably updated on Windows during this time, assert only that no error occurred
Expect(err).ToNot(HaveOccurred())
} else {
Expect(err).ToNot(HaveOccurred())
Expect(time2.PercentUserTime).To(BeNumerically(">", 0))
Expect(time2.PercentTotalTime).To(BeNumerically(">", 0))
}

err = time.Get(invalidPid)
invalidTime := ProcTime{}
err = invalidTime.Get(invalidPid)
Expect(err).To(HaveOccurred())
})

Expand Down
32 changes: 32 additions & 0 deletions sigar_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package sigar
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
Expand All @@ -13,6 +14,7 @@ import (
"strconv"
"strings"
"syscall"
"time"
)

var system struct {
Expand Down Expand Up @@ -705,6 +707,7 @@ func (self *ProcTime) Get(pid int) error {
return err
}

self.CollectionTime = time.Now()
fields := strings.Fields(string(contents))

user, _ := strtoull(fields[13])
Expand All @@ -720,6 +723,35 @@ func (self *ProcTime) Get(pid int) error {
self.StartTime += system.btime
self.StartTime *= 1000

return err
}

// Calculate percent of CPU usage by diffing counters. The "other" ProcTime should be from an earlier reading.
func (self *ProcTime) CalculateCpuPercent(other *ProcTime) error {
// The diffs need to be protected against underflow
if other.User > self.User {
return errors.New("Failed to calculate PercentUserTime: operation would result in underflow")
}
if other.Sys > self.Sys {
return errors.New("Failed to calculate PercentSysTime: operation would result in underflow")
}
if other.CollectionTime.After(self.CollectionTime) {
return errors.New("'Other' ProcTime should be older than current")
}

diffUser := uint64(self.User - other.User)
diffSys := uint64(self.Sys - other.Sys)
diffMillis := uint64(self.CollectionTime.Sub(other.CollectionTime) / time.Millisecond)

if diffMillis == 0 {
return errors.New("Failed to determine elapsed millisecods: operation would result in divide by zero")
}

// Calculate percentages, multiplying by 100 first to avoid precision loss
self.PercentUserTime = (diffUser * 100) / diffMillis
self.PercentSysTime = (diffSys * 100) / diffMillis
self.PercentTotalTime = ((diffUser + diffSys) * 100) / diffMillis

return nil
}

Expand Down
55 changes: 41 additions & 14 deletions sigar_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -992,27 +992,54 @@ DISTRIB_DESCRIPTION="Ubuntu 12.04.5 LTS"
})

It("GetsProcessTime", func() {
// Write cpu stats file /proc/stat
cpuFile := procd + "/stat"
cpuLine1 := "cpu 25 1 2 3 4 5 6 7\nbtime 1494680071"
err := ioutil.WriteFile(cpuFile, []byte(cpuLine1), 0644)
sigar.LoadStartTime()

// Write statFile /proc/10/stat
statFile := procd + "/10/stat"
statLine := "10 (watchdog/1) S 2 0 0 11 -1 2216722752 0 0 0 0 100 142 0 0 -100 0 1 0 40000 240 160 18446744073709551615 0 0 0 0 0 0 0 2147483647 0 18446744073709551615 0 0 17 1 99 1 0 0 0"
err := os.MkdirAll(procd+"/10/", 0777)
statLine := "10 (stress) R 25372 25372 10153 34819 25372 4202560 34 0 0 0 7238 16 0 0 20 0 1 0 29081667 6676480 50 18446744073709551615 4194304 4213484 140721323475968 140721323475512 140017284985275 0 0 0 0 0 0 0 17 0 0 0 0 0 0"
err = os.MkdirAll(procd+"/10/", 0777)
Expect(err).ToNot(HaveOccurred())
err = ioutil.WriteFile(statFile, []byte(statLine), 0444)
err = ioutil.WriteFile(statFile, []byte(statLine), 0644)
Expect(err).ToNot(HaveOccurred())
// Process start time is relative to system start time, we need to reset the system start time
bTimeFile := procd + "/stat"
bTimeContent := "btime: 0"
err = ioutil.WriteFile(bTimeFile, []byte(bTimeContent), 0444)

procTime1 := &sigar.ProcTime{}
err = procTime1.Get(10)
Expect(err).ToNot(HaveOccurred())
sigar.LoadStartTime()

procTime := &sigar.ProcTime{}
err = procTime.Get(10)
Expect(procTime1.User).To(Equal(uint64(72380)))
Expect(procTime1.Sys).To(Equal(uint64(160)))
Expect(procTime1.Total).To(Equal(uint64(72540)))
Expect(procTime1.StartTime).To(Equal(uint64(1494970887000)))

Expect(procTime1.PercentUserTime).To(Equal(uint64(0)))
Expect(procTime1.PercentSysTime).To(Equal(uint64(0)))
Expect(procTime1.PercentTotalTime).To(Equal(uint64(0)))

// Write updated version of statsFile
statLine2 := "10 (stress) R 25372 25372 10153 34819 25372 4202560 34 0 0 0 7337 17 0 0 20 0 1 0 29081667 6676480 50 18446744073709551615 4194304 4213484 140721323475968 140721323475512 140017284985305 0 0 0 0 0 0 0 17 0 0 0 0 0 0"
err = ioutil.WriteFile(statFile, []byte(statLine2), 0644)
Expect(err).ToNot(HaveOccurred())

procTime2 := &sigar.ProcTime{}
err = procTime2.Get(10)
Expect(err).ToNot(HaveOccurred())

Expect(procTime.User).To(Equal(uint64(1000)))
Expect(procTime.Sys).To(Equal(uint64(1420)))
Expect(procTime.Total).To(Equal(uint64(2420)))
Expect(procTime.StartTime).To(Equal(uint64(400000)))
procTime2.CollectionTime = procTime1.CollectionTime.Add(time.Second) // Simulate passing of 1 second
err = procTime2.CalculateCpuPercent(procTime1)
Expect(err).ToNot(HaveOccurred())

Expect(procTime2.User).To(Equal(uint64(73370)))
Expect(procTime2.Sys).To(Equal(uint64(170)))
Expect(procTime2.Total).To(Equal(uint64(73540)))
Expect(procTime2.StartTime).To(Equal(uint64(1494970887000)))

Expect(procTime2.PercentUserTime).To(Equal(uint64(99)))
Expect(procTime2.PercentSysTime).To(Equal(uint64(1)))
Expect(procTime2.PercentTotalTime).To(Equal(uint64(100)))
})

It("GetsProcessMemory", func() {
Expand Down
36 changes: 36 additions & 0 deletions sigar_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,15 @@ type Win32_Process struct {
WriteTransferCount uint64
}

// Used by the WMI package
type Win32_PerfFormattedData_PerfProc_Process struct {
IDProcess uint32
PercentPrivilegedTime uint64
PercentUserTime uint64
PercentProcessorTime uint64
PageFileBytes uint64
}

type WindowsRunState int

const (
Expand Down Expand Up @@ -370,6 +379,7 @@ func convert100NsUnitsToMillis(value uint64) uint64 {
}

func (self *ProcessList) Get() error {
// Query process list
var procs []Win32_Process
whereClause := ""
query := wmi.CreateQuery(&procs, whereClause)
Expand All @@ -378,9 +388,26 @@ func (self *ProcessList) Get() error {
return err
}

// Query performance class to get percent user/sys time
var procPerfs []Win32_PerfFormattedData_PerfProc_Process
whereClause = ""
query = wmi.CreateQuery(&procPerfs, whereClause)
err = wmiClient.Query(query, &procPerfs)
if err != nil {
return err
}

// Index performance data by process ID for easy lookup
perfLookup := make(map[uint32]Win32_PerfFormattedData_PerfProc_Process)
for _, procPerf := range procPerfs {
perfLookup[procPerf.IDProcess] = procPerf
}

// Form list of returned Process structs
processes := make([]Process, 0, len(procs))
for _, proc := range procs {
var process Process
perf := perfLookup[proc.ProcessId]

// ProcState
process.ProcState.Name = proc.Name
Expand All @@ -399,13 +426,17 @@ func (self *ProcessList) Get() error {
process.ProcMem.Size = proc.VirtualSize
process.ProcMem.Resident = proc.WorkingSetSize
process.ProcMem.PageFaults = uint64(proc.PageFaults)
process.ProcMem.PageFileBytes = perf.PageFileBytes

// ProcTime
process.ProcTime.User = convert100NsUnitsToMillis(proc.UserModeTime)
process.ProcTime.Sys = convert100NsUnitsToMillis(proc.KernelModeTime)
process.ProcTime.Total = process.ProcTime.User + process.ProcTime.Sys
// Convert proc.CreationDate to millis
process.ProcTime.StartTime = uint64(proc.CreationDate.UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond)))
process.ProcTime.PercentUserTime = perf.PercentUserTime
process.ProcTime.PercentSysTime = perf.PercentPrivilegedTime
process.ProcTime.PercentTotalTime = perf.PercentProcessorTime

// ProcArgs
process.ProcArgs.List = []string{proc.CommandLine}
Expand Down Expand Up @@ -514,6 +545,11 @@ func (self *ProcTime) Get(pid int) error {
return nil
}

func (self *ProcTime) CalculateCpuPercent(other *ProcTime) error {
// CPU Percentage is already provided by Get()
return nil
}

func (self *ProcArgs) Get(pid int) error {
proc, err := getWmiWin32ProcessResult(pid)
if err != nil {
Expand Down

0 comments on commit ca14ae9

Please sign in to comment.