diff --git a/README.md b/README.md index e6c5e4ec..b2eda56b 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ Grofer A clean system monitor and profiler written purely in golang using [termui](https://github.com/gizak/termui) and [gopsutil](https://github.com/shirou/gopsutil)! +Currently compatible with Linux only. + Installation ------------ @@ -35,6 +37,8 @@ cd grofer go build grofer.go ``` +--- + Usage ----- @@ -52,6 +56,7 @@ Available Commands: Flags: --config string config file (default is $HOME/.grofer.yaml) + -c, --cpuinfo Info about the CPU Load over all CPUs -h, --help help for grofer -r, --refresh int32 Overall stats UI refreshes rate in milliseconds greater than 1000 (default 1000) -t, --toggle Help message for toggle @@ -60,11 +65,13 @@ Use "grofer [command] --help" for more information about a command. ``` +--- + Examples -------- -`grofer [-r refreshRate]` -------------------------- +`grofer [-r refreshRate] [-c]` +------------------------------ This gives overall utilization stats refreshed every `refreshRate` milliseconds. Default and minimum value of the refresh rate is `1000 ms`. @@ -76,6 +83,21 @@ Information provided: - Network usage - Disk storage +The `-c, --cpuinfo` flag displays finer details about the CPU load such as percentage of the time spent servicing software interrupts, hardware interrupts, etc. + +![grofer-cpu](images/README/cpuload.png) + +Information provided: +- Usr : % of time spent executing user level applications. +- Sys : % of time spent executing kernel level processes. +- Irq : % of time spent servicing hardware interrupts. +- Idle : % of time CPU was idle. +- Nice : % of time spent by CPU executing user level processes with a nice priority. +- Iowait: % of time spent by CPU waiting for an outstanding disk I/O. +- Soft : % of time spent by the CPU servicing software interrupts. + +- Steal : % of time spent in involuntary waiting by logical CPUs. + --- `grofer proc [-p PID] [-r refreshRate]` diff --git a/cmd/root.go b/cmd/root.go index 89e043b2..9e1c272c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -26,6 +26,7 @@ import ( overallGraph "github.com/pesos/grofer/src/display/general" "github.com/pesos/grofer/src/general" + info "github.com/pesos/grofer/src/general" "github.com/pesos/grofer/src/utils" ) @@ -40,22 +41,38 @@ var rootCmd = &cobra.Command{ Use: "grofer", Short: "grofer is a system profiler written in golang", RunE: func(cmd *cobra.Command, args []string) error { - overallRefreshRate, _ := cmd.Flags().GetInt32("refresh") if overallRefreshRate < 1000 { return fmt.Errorf("invalid refresh rate: minimum refresh rate is 1000(ms)") } var wg sync.WaitGroup - endChannel := make(chan os.Signal, 1) - dataChannel := make(chan utils.DataStats, 1) - wg.Add(2) + cpuLoadFlag, _ := cmd.Flags().GetBool("cpuinfo") + if cpuLoadFlag { + cpuLoad := info.NewCPULoad() + dataChannel := make(chan *info.CPULoad, 1) + endChannel := make(chan os.Signal, 1) + + wg.Add(2) + + go info.GetCPULoad(cpuLoad, dataChannel, endChannel, int32(4*overallRefreshRate/5), &wg) + + go overallGraph.RenderCPUinfo(endChannel, dataChannel, overallRefreshRate, &wg) - go general.GlobalStats(endChannel, dataChannel, int32(4*overallRefreshRate/5), &wg) - go overallGraph.RenderCharts(endChannel, dataChannel, overallRefreshRate, &wg) + wg.Wait() - wg.Wait() + } else { + endChannel := make(chan os.Signal, 1) + dataChannel := make(chan utils.DataStats, 1) + + wg.Add(2) + + go general.GlobalStats(endChannel, dataChannel, int32(4*overallRefreshRate/5), &wg) + go overallGraph.RenderCharts(endChannel, dataChannel, overallRefreshRate, &wg) + + wg.Wait() + } return nil }, @@ -73,6 +90,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.grofer.yaml)") rootCmd.Flags().Int32P("refresh", "r", DefaultOverallRefreshRate, "Overall stats UI refreshes rate in milliseconds greater than 1000") + rootCmd.Flags().BoolP("cpuinfo", "c", false, "Info about the CPU Load over all CPUs") rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/go.mod b/go.mod index 0cd11075..3130e8ba 100644 --- a/go.mod +++ b/go.mod @@ -11,5 +11,7 @@ require ( github.com/shirou/gopsutil v2.20.6+incompatible github.com/spf13/cobra v1.0.0 github.com/spf13/viper v1.7.0 + github.com/thedevsaddam/gojsonq/v2 v2.5.2 + github.com/tidwall/gjson v1.6.0 golang.org/x/sys v0.0.0-20200722175500-76b94024e4b6 // indirect ) diff --git a/go.sum b/go.sum index 127ff4b6..00f8b7f9 100644 --- a/go.sum +++ b/go.sum @@ -204,6 +204,16 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/thedevsaddam/gojsonq v1.9.1 h1:zQulEP43nwmq5EKrNWyIgJVbqDeMdC1qzXM/f5O15a0= +github.com/thedevsaddam/gojsonq v2.3.0+incompatible h1:i2lFTvGY4LvoZ2VUzedsFlRiyaWcJm3Uh6cQ9+HyQA8= +github.com/thedevsaddam/gojsonq/v2 v2.5.2 h1:CoMVaYyKFsVj6TjU6APqAhAvC07hTI6IQen8PHzHYY0= +github.com/thedevsaddam/gojsonq/v2 v2.5.2/go.mod h1:bv6Xa7kWy82uT0LnXPE2SzGqTj33TAEeR560MdJkiXs= +github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc= +github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= +github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= diff --git a/images/README/cpuload.png b/images/README/cpuload.png new file mode 100644 index 00000000..01e578c7 Binary files /dev/null and b/images/README/cpuload.png differ diff --git a/src/display/general/init.go b/src/display/general/init.go index 2677f74b..2b384ad6 100644 --- a/src/display/general/init.go +++ b/src/display/general/init.go @@ -32,6 +32,19 @@ type MainPage struct { NetPara *widgets.Paragraph } +type CPUPage struct { + Grid *ui.Grid + UsrChart *widgets.Gauge + NiceChart *widgets.Gauge + SysChart *widgets.Gauge + IowaitChart *widgets.Gauge + IrqChart *widgets.Gauge + SoftChart *widgets.Gauge + IdleChart *widgets.Gauge + StealChart *widgets.Gauge + CPUChart *widgets.Table +} + // NewPage returns a new page initialized from the MainPage struct func NewPage(numCores int) *MainPage { page := &MainPage{ @@ -46,6 +59,23 @@ func NewPage(numCores int) *MainPage { return page } +func NewCPUPage(numCores int) *CPUPage { + page := &CPUPage{ + Grid: ui.NewGrid(), + UsrChart: widgets.NewGauge(), + NiceChart: widgets.NewGauge(), + SysChart: widgets.NewGauge(), + IowaitChart: widgets.NewGauge(), + IrqChart: widgets.NewGauge(), + SoftChart: widgets.NewGauge(), + IdleChart: widgets.NewGauge(), + StealChart: widgets.NewGauge(), + CPUChart: widgets.NewTable(), + } + page.InitCPU(numCores) + return page +} + // InitGeneral initializes all ui elements for the ui rendered by the grofer command func (page *MainPage) InitGeneral(numCores int) { @@ -108,3 +138,89 @@ func (page *MainPage) InitGeneral(numCores int) { w, h := ui.TerminalDimensions() page.Grid.SetRect(w/2, 0, w, h) } + +func (page *CPUPage) InitCPU(numCores int) { + page.UsrChart.Title = " Usr " + page.UsrChart.Percent = 0 + page.UsrChart.BarColor = ui.ColorBlue + page.UsrChart.BorderStyle.Fg = ui.ColorCyan + page.UsrChart.TitleStyle.Fg = ui.ColorWhite + + page.NiceChart.Title = " Nice " + page.NiceChart.Percent = 0 + page.NiceChart.BarColor = ui.ColorBlue + page.NiceChart.BorderStyle.Fg = ui.ColorCyan + page.NiceChart.TitleStyle.Fg = ui.ColorWhite + + page.SysChart.Title = " Sys " + page.SysChart.Percent = 0 + page.SysChart.BarColor = ui.ColorBlue + page.SysChart.BorderStyle.Fg = ui.ColorCyan + page.SysChart.TitleStyle.Fg = ui.ColorWhite + + page.IowaitChart.Title = " Iowait " + page.IowaitChart.Percent = 0 + page.IowaitChart.BarColor = ui.ColorBlue + page.IowaitChart.BorderStyle.Fg = ui.ColorCyan + page.IowaitChart.TitleStyle.Fg = ui.ColorWhite + + page.IrqChart.Title = " Irq " + page.IrqChart.Percent = 0 + page.IrqChart.BarColor = ui.ColorBlue + page.IrqChart.BorderStyle.Fg = ui.ColorCyan + page.IrqChart.TitleStyle.Fg = ui.ColorWhite + + page.SoftChart.Title = " Soft " + page.SoftChart.Percent = 0 + page.SoftChart.BarColor = ui.ColorBlue + page.SoftChart.BorderStyle.Fg = ui.ColorCyan + page.SoftChart.TitleStyle.Fg = ui.ColorWhite + + page.IdleChart.Title = " Idle " + page.IdleChart.Percent = 0 + page.IdleChart.BarColor = ui.ColorBlue + page.IdleChart.BorderStyle.Fg = ui.ColorCyan + page.IdleChart.TitleStyle.Fg = ui.ColorWhite + + page.StealChart.Title = " Steal " + page.StealChart.Percent = 0 + page.StealChart.BarColor = ui.ColorBlue + page.StealChart.BorderStyle.Fg = ui.ColorCyan + page.StealChart.TitleStyle.Fg = ui.ColorWhite + + page.CPUChart.Title = " CPU " + page.CPUChart.TextStyle = ui.NewStyle(ui.ColorWhite) + page.CPUChart.TextAlignment = ui.AlignCenter + page.CPUChart.RowSeparator = true + + columnWidths := []int{} + for i := 0; i < numCores; i++ { + columnWidths = append(columnWidths, 9) + } + + page.CPUChart.ColumnWidths = columnWidths + + page.Grid.Set( + ui.NewRow(0.17, + ui.NewCol(0.5, page.UsrChart), + ui.NewCol(0.5, page.NiceChart), + ), + ui.NewRow(0.17, + ui.NewCol(0.5, page.SysChart), + ui.NewCol(0.5, page.IowaitChart), + ), + ui.NewRow(0.17, + ui.NewCol(0.5, page.IrqChart), + ui.NewCol(0.5, page.SoftChart), + ), + ui.NewRow(0.17, + ui.NewCol(0.5, page.IdleChart), + ui.NewCol(0.5, page.StealChart), + ), + ui.NewRow(0.30, page.CPUChart), + ) + + w, h := ui.TerminalDimensions() + page.Grid.SetRect(0, 0, w, h) + +} diff --git a/src/display/general/overallGraphs.go b/src/display/general/overallGraphs.go index 680884ac..538344aa 100644 --- a/src/display/general/overallGraphs.go +++ b/src/display/general/overallGraphs.go @@ -26,6 +26,7 @@ import ( "time" ui "github.com/gizak/termui/v3" + info "github.com/pesos/grofer/src/general" "github.com/pesos/grofer/src/utils" ) @@ -44,6 +45,7 @@ func RenderCharts(endChannel chan os.Signal, } defer ui.Close() + var on sync.Once var totalBytesRecv float64 var totalBytesSent float64 @@ -176,6 +178,32 @@ func RenderCharts(endChannel chan os.Signal, myPage.NetworkChart.Data = temp } + + on.Do(func() { + // Get Terminal Dimensions adn clear the UI + w, h := ui.TerminalDimensions() + ui.Clear() + + // Calculate Heigth offset + height := int(h / numCores) + heightOffset := h - (height * numCores) + + // Adjust Memory Bar graph values + myPage.MemoryChart.BarGap = ((w / 2) - (4 * myPage.MemoryChart.BarWidth)) / 4 + + // Adjust CPU Gauge dimensions + if isCPUSet { + for i := 0; i < numCores; i++ { + myPage.CPUCharts[i].SetRect(0, i*height, w/2, (i+1)*height) + ui.Render(myPage.CPUCharts[i]) + } + } + + // Adjust Grid dimensions + myPage.Grid.SetRect(w/2, 0, w, h-heightOffset) + + ui.Render(myPage.Grid) + }) } case <-tick: // Update page with new values @@ -183,3 +211,77 @@ func RenderCharts(endChannel chan os.Signal, } } } + +func RenderCPUinfo(endChannel chan os.Signal, + dataChannel chan *info.CPULoad, + refreshRate int32, + wg *sync.WaitGroup) { + + var on sync.Once + + if err := ui.Init(); err != nil { + log.Fatalf("failed to initialize termui: %v", err) + } + defer ui.Close() + + numCores := runtime.NumCPU() + myPage := NewCPUPage(numCores) + + pause := func() { + run = !run + } + + // Re render UI + updateUI := func() { + w, h := ui.TerminalDimensions() + ui.Clear() + myPage.Grid.SetRect(0, 0, w, h) + ui.Render(myPage.Grid) + } + + updateUI() + + uiEvents := ui.PollEvents() + tick := time.Tick(time.Duration(refreshRate) * time.Millisecond) + for { + select { + case e := <-uiEvents: // For keyboard events + switch e.ID { + case "q", "": // q or Ctrl-C to quit + endChannel <- os.Kill + wg.Done() + return + + case "": + updateUI() + + case "s": // s to stop + pause() + } + + case data := <-dataChannel: // Update chart values + if run { + myPage.UsrChart.Percent = data.Usr + myPage.NiceChart.Percent = data.Nice + myPage.SysChart.Percent = data.Sys + myPage.IowaitChart.Percent = data.Iowait + myPage.IrqChart.Percent = data.Irq + myPage.SoftChart.Percent = data.Soft + myPage.StealChart.Percent = data.Steal + myPage.IdleChart.Percent = data.Idle + + myPage.CPUChart.Rows = data.CPURates + + on.Do(func() { + w, h := ui.TerminalDimensions() + ui.Clear() + myPage.Grid.SetRect(0, 0, w, h) + ui.Render(myPage.Grid) + }) + } + + case <-tick: + updateUI() + } + } +} diff --git a/src/display/process/allProcs.go b/src/display/process/allProcs.go index f6082e72..0cc55b3e 100644 --- a/src/display/process/allProcs.go +++ b/src/display/process/allProcs.go @@ -31,7 +31,6 @@ import ( ) var runAllProc = true -var on1 sync.Once func getData(procs []*proc.Process) []string { var data []string @@ -107,7 +106,7 @@ func getData(procs []*proc.Process) []string { createTime := utils.GetDateFromUnix(ctime) temp = temp + createTime } - for i := 0; i < 24-len(createTime); i++ { + for i := 0; i < 9-len(createTime); i++ { temp = temp + " " } @@ -132,6 +131,8 @@ func AllProcVisuals(dataChannel chan []*proc.Process, log.Fatalf("failed to initialize termui: %v", err) } + var on sync.Once + myPage := NewAllProcsPage() updateUI := func() { @@ -200,7 +201,7 @@ func AllProcVisuals(dataChannel chan []*proc.Process, if runAllProc { myPage.BodyList.Rows = getData(data) - on1.Do(func() { + on.Do(func() { w, h := ui.TerminalDimensions() ui.Clear() myPage.Grid.SetRect(0, 0, w, h) diff --git a/src/display/process/procGraphs.go b/src/display/process/procGraphs.go index 7005adbb..c84a21f6 100644 --- a/src/display/process/procGraphs.go +++ b/src/display/process/procGraphs.go @@ -28,7 +28,6 @@ import ( ) var runProc = true -var on sync.Once func getChildProcs(proc *process.Process) []string { childProcs := []string{"PID Command"} @@ -59,6 +58,8 @@ func ProcVisuals(endChannel chan os.Signal, log.Fatalf("failed to initialize termui: %v", err) } + var on sync.Once + // Create new page myPage := NewPerProcPage() diff --git a/src/general/cpuInfo.go b/src/general/cpuInfo.go new file mode 100644 index 00000000..9c2156a2 --- /dev/null +++ b/src/general/cpuInfo.go @@ -0,0 +1,112 @@ +/* +Copyright © 2020 The PES Open Source Team pesos@pes.edu + +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. +*/ + +package general + +import ( + "fmt" + "os" + "os/exec" + "strconv" + "sync" + "time" + + gjson "github.com/tidwall/gjson" +) + +// CPULoad type contains info about load on CPU from various sources +// as well as general stats about the CPU. +type CPULoad struct { + Usr int `json:"usr"` + Nice int `json:"nice"` + Sys int `json:"sys"` + Iowait int `json:"iowait"` + Irq int `json:"irq"` + Soft int `json:"soft"` + Steal int `json:"steal"` + Guest int `json:"guest"` + Gnice int `json:"gnice"` + Idle int `json:"idle"` + CPURates [][]string `json:"-"` // Has first row with CPU names and second row with CPU usage rates, might not be ideal format for export +} + +func NewCPULoad() *CPULoad { + return &CPULoad{} +} + +func (c *CPULoad) updateCPULoad() error { + mpstat := "mpstat" + arg0 := "-o" + arg1 := "JSON" + cmd := exec.Command(mpstat, arg0, arg1) + stdout, err := cmd.Output() + if err != nil { + return err + } + + statsExtract := gjson.Get(string(stdout), "sysstat.hosts.0.statistics.0.cpu-load.0") + stats := statsExtract.Map() + c.Usr = int(stats["usr"].Int()) + c.Nice = int(stats["nice"].Int()) + c.Sys = int(stats["sys"].Int()) + c.Iowait = int(stats["iowait"].Int()) + c.Irq = int(stats["irq"].Int()) + c.Soft = int(stats["soft"].Int()) + c.Steal = int(stats["steal"].Int()) + c.Guest = int(stats["guest"].Int()) + c.Gnice = int(stats["gnice"].Int()) + c.Idle = int(stats["idle"].Int()) + + cpuRates, err := GetCPURates() + if err != nil { + return err + } + + rate := []string{} + cpus := []string{} + for i, cpuRate := range cpuRates { + cpus = append(cpus, "CPU "+strconv.Itoa(i)) + rate = append(rate, fmt.Sprintf("%.2f", cpuRate)) + } + rates := [][]string{cpus, rate} + + c.CPURates = rates + + return nil +} + +// GetCPULoad updated the CPULoad struct and serves the data to the data channel. +func GetCPULoad(cpuLoad *CPULoad, + dataChannel chan *CPULoad, + endChannel chan os.Signal, + refreshRate int32, + wg *sync.WaitGroup) error { + for { + select { + case <-endChannel: // Stop execution if end signal received + wg.Done() + return nil + + default: // Get Memory and CPU rates per core periodically + err := cpuLoad.updateCPULoad() + if err != nil { + return err + } + dataChannel <- cpuLoad + time.Sleep(time.Duration(refreshRate) * time.Millisecond) + } + } +} diff --git a/src/general/generalStats.go b/src/general/generalStats.go index bd04b70d..d5fe9437 100644 --- a/src/general/generalStats.go +++ b/src/general/generalStats.go @@ -36,10 +36,10 @@ func GlobalStats(endChannel chan os.Signal, default: // Get Memory and CPU rates per core periodically - go PrintCPURates(dataChannel) - go PrintMemRates(dataChannel) - go PrintDiskRates(dataChannel) - PrintNetRates(dataChannel) + go ServeCPURates(dataChannel) + go ServeMemRates(dataChannel) + go ServeDiskRates(dataChannel) + ServeNetRates(dataChannel) time.Sleep(time.Duration(refreshRate) * time.Millisecond) } } diff --git a/src/general/printStats.go b/src/general/serveStats.go similarity index 80% rename from src/general/printStats.go rename to src/general/serveStats.go index bcefeea3..6d4bf645 100644 --- a/src/general/printStats.go +++ b/src/general/serveStats.go @@ -34,8 +34,17 @@ func roundOff(num uint64) float64 { return math.Round(x*10) / 10 } -// PrintCPURates print the cpu rates -func PrintCPURates(cpuChannel chan utils.DataStats) { +// GetCPURates fetches and returns the current cpu rate +func GetCPURates() ([]float64, error) { + cpuRates, err := cpu.Percent(time.Second, true) + if err != nil { + return nil, err + } + return cpuRates, nil +} + +// ServeCPURates serves the cpu rates to the cpu channel +func ServeCPURates(cpuChannel chan utils.DataStats) { cpuRates, err := cpu.Percent(time.Second, true) if err != nil { log.Fatal(err) @@ -47,8 +56,8 @@ func PrintCPURates(cpuChannel chan utils.DataStats) { cpuChannel <- data } -// PrintMemRates prints stats about the memory -func PrintMemRates(dataChannel chan utils.DataStats) { +// ServeMemRates serves stats about the memory to the data channel +func ServeMemRates(dataChannel chan utils.DataStats) { memory, err := mem.VirtualMemory() if err != nil { log.Fatal(err) @@ -64,7 +73,8 @@ func PrintMemRates(dataChannel chan utils.DataStats) { dataChannel <- data } -func PrintDiskRates(dataChannel chan utils.DataStats) { +// ServeDiskRates serves the disk rate data to the data channel +func ServeDiskRates(dataChannel chan utils.DataStats) { var partitions []disk.PartitionStat var err error @@ -103,7 +113,8 @@ func PrintDiskRates(dataChannel chan utils.DataStats) { dataChannel <- data } -func PrintNetRates(dataChannel chan utils.DataStats) { +// ServeNetRates serves info about the network to the data channel +func ServeNetRates(dataChannel chan utils.DataStats) { netStats, err := net.IOCounters(false) if err != nil { log.Fatal(err)