1
0
mirror of https://github.com/google/gops.git synced 2024-11-27 08:31:17 +02:00
gops/main.go
Tobias Klauser 8918f15c19 Allow to specify CPU usage period as duration string
The current implementation assumes the given duration in seconds.
Instead, also allow to specify a duration in the format as expected by
`time.ParseDuration` and use that if found. If a plain integer is
specified, it is still parsed as a duration in seconds.
2021-07-01 12:01:38 +02:00

303 lines
7.3 KiB
Go

// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Program gops is a tool to list currently running Go processes.
package main
import (
"bytes"
"fmt"
"log"
"math"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/google/gops/goprocess"
"github.com/shirou/gopsutil/v3/process"
"github.com/xlab/treeprint"
)
const helpText = `gops is a tool to list and diagnose Go processes.
Usage:
gops <cmd> <pid|addr> ...
gops <pid> # displays process info
gops help # displays this help message
Commands:
stack Prints the stack trace.
gc Runs the garbage collector and blocks until successful.
setgc Sets the garbage collection target percentage.
memstats Prints the allocation and garbage collection stats.
version Prints the Go version used to build the program.
stats Prints runtime stats.
trace Runs the runtime tracer for 5 secs and launches "go tool trace".
pprof-heap Reads the heap profile and launches "go tool pprof".
pprof-cpu Reads the CPU profile and launches "go tool pprof".
All commands require the agent running on the Go process.
"*" indicates the process is running the agent.`
// TODO(jbd): add link that explains the use of agent.
func main() {
if len(os.Args) < 2 {
processes()
return
}
cmd := os.Args[1]
// See if it is a PID.
pid, err := strconv.Atoi(cmd)
if err == nil {
var period time.Duration
if len(os.Args) >= 3 {
period, err = time.ParseDuration(os.Args[2])
if err != nil {
secs, _ := strconv.Atoi(os.Args[2])
period = time.Duration(secs) * time.Second
}
}
processInfo(pid, period)
return
}
if cmd == "help" {
usage("")
}
if cmd == "tree" {
displayProcessTree()
return
}
fn, ok := cmds[cmd]
if !ok {
usage("unknown subcommand")
}
if len(os.Args) < 3 {
usage("Missing PID or address.")
os.Exit(1)
}
addr, err := targetToAddr(os.Args[2])
if err != nil {
fmt.Fprintf(os.Stderr, "Couldn't resolve addr or pid %v to TCPAddress: %v\n", os.Args[2], err)
os.Exit(1)
}
var params []string
if len(os.Args) > 3 {
params = append(params, os.Args[3:]...)
}
if err := fn(*addr, params); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
func processes() {
ps := goprocess.FindAll()
var maxPID, maxPPID, maxExec, maxVersion int
for i, p := range ps {
ps[i].BuildVersion = shortenVersion(p.BuildVersion)
maxPID = max(maxPID, len(strconv.Itoa(p.PID)))
maxPPID = max(maxPPID, len(strconv.Itoa(p.PPID)))
maxExec = max(maxExec, len(p.Exec))
maxVersion = max(maxVersion, len(ps[i].BuildVersion))
}
for _, p := range ps {
buf := bytes.NewBuffer(nil)
pid := strconv.Itoa(p.PID)
fmt.Fprint(buf, pad(pid, maxPID))
fmt.Fprint(buf, " ")
ppid := strconv.Itoa(p.PPID)
fmt.Fprint(buf, pad(ppid, maxPPID))
fmt.Fprint(buf, " ")
fmt.Fprint(buf, pad(p.Exec, maxExec))
if p.Agent {
fmt.Fprint(buf, "*")
} else {
fmt.Fprint(buf, " ")
}
fmt.Fprint(buf, " ")
fmt.Fprint(buf, pad(p.BuildVersion, maxVersion))
fmt.Fprint(buf, " ")
fmt.Fprint(buf, p.Path)
fmt.Fprintln(buf)
buf.WriteTo(os.Stdout)
}
}
func processInfo(pid int, period time.Duration) {
p, err := process.NewProcess(int32(pid))
if err != nil {
log.Fatalf("Cannot read process info: %v", err)
}
if v, err := p.Parent(); err == nil {
fmt.Printf("parent PID:\t%v\n", v.Pid)
}
if v, err := p.NumThreads(); err == nil {
fmt.Printf("threads:\t%v\n", v)
}
if v, err := p.MemoryPercent(); err == nil {
fmt.Printf("memory usage:\t%.3f%%\n", v)
}
if v, err := p.CPUPercent(); err == nil {
fmt.Printf("cpu usage:\t%.3f%%\n", v)
}
if period > 0 {
if v, err := cpuPercentWithinTime(p, period); err == nil {
fmt.Printf("cpu usage (%v):\t%.3f%%\n", period, v)
}
}
if v, err := p.Username(); err == nil {
fmt.Printf("username:\t%v\n", v)
}
if v, err := p.Cmdline(); err == nil {
fmt.Printf("cmd+args:\t%v\n", v)
}
if v, err := elapsedTime(p); err == nil {
fmt.Printf("elapsed time:\t%v\n", v)
}
if v, err := p.Connections(); err == nil {
if len(v) > 0 {
for _, conn := range v {
fmt.Printf("local/remote:\t%v:%v <-> %v:%v (%v)\n",
conn.Laddr.IP, conn.Laddr.Port, conn.Raddr.IP, conn.Raddr.Port, conn.Status)
}
}
}
}
// cpuPercentWithinTime return how many percent of the CPU time this process uses within given time duration
func cpuPercentWithinTime(p *process.Process, t time.Duration) (float64, error) {
cput, err := p.Times()
if err != nil {
return 0, err
}
time.Sleep(t)
cput2, err := p.Times()
if err != nil {
return 0, err
}
return 100 * (cput2.Total() - cput.Total()) / t.Seconds(), nil
}
// elapsedTime shows the elapsed time of the process indicating how long the
// process has been running for.
func elapsedTime(p *process.Process) (string, error) {
crtTime, err := p.CreateTime()
if err != nil {
return "", err
}
etime := time.Since(time.Unix(crtTime/1000, 0))
return fmtEtimeDuration(etime), nil
}
// pstree contains a mapping between the PPIDs and the child processes.
var pstree map[int][]goprocess.P
// displayProcessTree displays a tree of all the running Go processes.
func displayProcessTree() {
ps := goprocess.FindAll()
pstree = make(map[int][]goprocess.P)
for _, p := range ps {
pstree[p.PPID] = append(pstree[p.PPID], p)
}
tree := treeprint.New()
tree.SetValue("...")
seen := map[int]bool{}
for _, p := range ps {
constructProcessTree(p.PPID, p, seen, tree)
}
fmt.Println(tree.String())
}
// constructProcessTree constructs the process tree in a depth-first fashion.
func constructProcessTree(ppid int, process goprocess.P, seen map[int]bool, tree treeprint.Tree) {
if seen[ppid] {
return
}
seen[ppid] = true
if ppid != process.PPID {
output := strconv.Itoa(ppid) + " (" + process.Exec + ")" + " {" + process.BuildVersion + "}"
if process.Agent {
tree = tree.AddMetaBranch("*", output)
} else {
tree = tree.AddBranch(output)
}
} else {
tree = tree.AddBranch(ppid)
}
for index := range pstree[ppid] {
process := pstree[ppid][index]
constructProcessTree(process.PID, process, seen, tree)
}
}
var develRe = regexp.MustCompile(`devel\s+\+\w+`)
func shortenVersion(v string) string {
if !strings.HasPrefix(v, "devel") {
return v
}
results := develRe.FindAllString(v, 1)
if len(results) == 0 {
return v
}
return results[0]
}
func usage(msg string) {
// default exit code for the statement
exitCode := 0
if msg != "" {
// founded an unexpected command
fmt.Printf("gops: %v\n", msg)
exitCode = 1
}
fmt.Fprintf(os.Stderr, "%v\n", helpText)
os.Exit(exitCode)
}
func pad(s string, total int) string {
if len(s) >= total {
return s
}
return s + strings.Repeat(" ", total-len(s))
}
func max(i, j int) int {
if i > j {
return i
}
return j
}
// fmtEtimeDuration formats etime's duration based on ps' format:
// [[DD-]hh:]mm:ss
// format specification: http://linuxcommand.org/lc3_man_pages/ps1.html
func fmtEtimeDuration(d time.Duration) string {
days := d / (24 * time.Hour)
hours := d % (24 * time.Hour)
minutes := hours % time.Hour
seconds := math.Mod(minutes.Seconds(), 60)
var b strings.Builder
if days > 0 {
fmt.Fprintf(&b, "%02d-", days)
}
if days > 0 || hours/time.Hour > 0 {
fmt.Fprintf(&b, "%02d:", hours/time.Hour)
}
fmt.Fprintf(&b, "%02d:", minutes/time.Minute)
fmt.Fprintf(&b, "%02.0f", seconds)
return b.String()
}