229 lines
5.1 KiB
Go
229 lines
5.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"os/user"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// All sizes are in kB, matching /proc.
|
|
|
|
type SwapDev struct {
|
|
Path, Type string
|
|
Size, Used uint64
|
|
Prio int
|
|
Zram string // "orig→compressed" for zram devices, else ""
|
|
}
|
|
|
|
type Proc struct {
|
|
PID, PPID, UID int
|
|
Tree string // tree-view prefix, display only
|
|
User, Comm, Cmd string
|
|
Swap, SwapPss, RSS uint64
|
|
Delta int64 // change in Swap since previous sample
|
|
HasPss bool // smaps_rollup was readable
|
|
}
|
|
|
|
type Mapping struct {
|
|
Start, End uint64
|
|
Perms, Path, Flags string
|
|
Rss, Swap, SwapPss uint64
|
|
}
|
|
|
|
type Sys struct {
|
|
Mem map[string]uint64 // /proc/meminfo
|
|
Devs []SwapDev
|
|
Procs []Proc
|
|
ProcSwap uint64 // sum of per-process SwapPss (Swap where PSS unreadable)
|
|
Swapped int // processes with Swap > 0
|
|
Unreadable int // swapped processes whose smaps_rollup we could not read
|
|
}
|
|
|
|
func (s *Sys) SwapUsed() uint64 { return s.Mem["SwapTotal"] - s.Mem["SwapFree"] }
|
|
|
|
func collect(prev map[int]uint64) *Sys {
|
|
s := &Sys{Mem: parseKV(readFile("/proc/meminfo")), Devs: readSwaps()}
|
|
ents, _ := os.ReadDir("/proc")
|
|
for _, e := range ents {
|
|
pid, err := strconv.Atoi(e.Name())
|
|
if err != nil {
|
|
continue
|
|
}
|
|
p, ok := readProc(pid)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if pv, ok := prev[pid]; ok {
|
|
p.Delta = int64(p.Swap) - int64(pv)
|
|
}
|
|
if p.Swap > 0 {
|
|
s.Swapped++
|
|
if p.HasPss {
|
|
s.ProcSwap += p.SwapPss
|
|
} else {
|
|
s.ProcSwap += p.Swap
|
|
s.Unreadable++
|
|
}
|
|
}
|
|
s.Procs = append(s.Procs, p)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func readProc(pid int) (Proc, bool) {
|
|
dir := "/proc/" + strconv.Itoa(pid)
|
|
st := readFile(dir + "/status")
|
|
if st == nil {
|
|
return Proc{}, false
|
|
}
|
|
p := Proc{PID: pid}
|
|
for _, line := range strings.Split(string(st), "\n") {
|
|
k, v, ok := strings.Cut(line, ":")
|
|
if !ok {
|
|
continue
|
|
}
|
|
v = strings.TrimSpace(v)
|
|
switch k {
|
|
case "Name":
|
|
p.Comm = v
|
|
case "PPid":
|
|
p.PPID, _ = strconv.Atoi(v)
|
|
case "Uid":
|
|
p.UID, _ = strconv.Atoi(strings.Fields(v)[0])
|
|
case "VmRSS":
|
|
p.RSS = kb(v)
|
|
case "VmSwap":
|
|
p.Swap = kb(v)
|
|
}
|
|
}
|
|
p.User = userName(p.UID)
|
|
cmd := bytes.TrimRight(readFile(dir+"/cmdline"), "\x00")
|
|
p.Cmd = string(bytes.ReplaceAll(cmd, []byte{0}, []byte{' '}))
|
|
if p.Cmd == "" {
|
|
p.Cmd = "[" + p.Comm + "]"
|
|
}
|
|
if p.Swap > 0 {
|
|
if r := readFile(dir + "/smaps_rollup"); r != nil {
|
|
m := parseKV(r) // rollup Swap also counts swapped shmem this process maps; VmSwap is anon only
|
|
p.Swap, p.SwapPss, p.HasPss = m["Swap"], m["SwapPss"], true
|
|
}
|
|
}
|
|
return p, true
|
|
}
|
|
|
|
func readSmaps(pid int) ([]Mapping, error) {
|
|
b, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/smaps")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseSmaps(b), nil
|
|
}
|
|
|
|
func parseSmaps(b []byte) []Mapping {
|
|
var out []Mapping
|
|
for _, line := range strings.Split(string(b), "\n") {
|
|
f := strings.Fields(line)
|
|
if len(f) == 0 {
|
|
continue
|
|
}
|
|
if !strings.HasSuffix(f[0], ":") { // VMA header: start-end perms offset dev inode [path]
|
|
lo, hi, ok := strings.Cut(f[0], "-")
|
|
if !ok || len(f) < 5 {
|
|
continue
|
|
}
|
|
m := Mapping{Perms: f[1]}
|
|
m.Start, _ = strconv.ParseUint(lo, 16, 64)
|
|
m.End, _ = strconv.ParseUint(hi, 16, 64)
|
|
if len(f) > 5 {
|
|
m.Path = strings.Join(f[5:], " ")
|
|
}
|
|
out = append(out, m)
|
|
continue
|
|
}
|
|
if len(out) == 0 {
|
|
continue
|
|
}
|
|
m := &out[len(out)-1]
|
|
switch f[0] {
|
|
case "Rss:":
|
|
m.Rss = kb(f[1])
|
|
case "Swap:":
|
|
m.Swap = kb(f[1])
|
|
case "SwapPss:":
|
|
m.SwapPss = kb(f[1])
|
|
case "VmFlags:":
|
|
m.Flags = strings.Join(f[1:], " ")
|
|
}
|
|
}
|
|
sort.SliceStable(out, func(i, j int) bool { return out[i].Swap > out[j].Swap })
|
|
return out
|
|
}
|
|
|
|
func readSwaps() []SwapDev {
|
|
var devs []SwapDev
|
|
for i, line := range strings.Split(string(readFile("/proc/swaps")), "\n") {
|
|
f := strings.Fields(line)
|
|
if i == 0 || len(f) < 5 {
|
|
continue
|
|
}
|
|
d := SwapDev{Path: f[0], Type: f[1], Size: kb(f[2]), Used: kb(f[3])}
|
|
d.Prio, _ = strconv.Atoi(f[4])
|
|
if strings.HasPrefix(d.Path, "/dev/zram") {
|
|
// mm_stat: orig_data_size compr_data_size mem_used_total ... (bytes)
|
|
if f := strings.Fields(string(readFile("/sys/block/" + strings.TrimPrefix(d.Path, "/dev/") + "/mm_stat"))); len(f) >= 3 {
|
|
o, _ := strconv.ParseUint(f[0], 10, 64)
|
|
c, _ := strconv.ParseUint(f[2], 10, 64)
|
|
d.Zram = human(o/1024) + "→" + human(c/1024) + " RAM"
|
|
}
|
|
}
|
|
devs = append(devs, d)
|
|
}
|
|
return devs
|
|
}
|
|
|
|
// parseKV parses "Key: 123 kB" style files (meminfo, smaps_rollup) into kB values.
|
|
func parseKV(b []byte) map[string]uint64 {
|
|
m := map[string]uint64{}
|
|
for _, line := range strings.Split(string(b), "\n") {
|
|
k, v, ok := strings.Cut(line, ":")
|
|
if ok {
|
|
m[k] = kb(v)
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func kb(v string) uint64 {
|
|
f := strings.Fields(v)
|
|
if len(f) == 0 {
|
|
return 0
|
|
}
|
|
n, _ := strconv.ParseUint(f[0], 10, 64)
|
|
return n
|
|
}
|
|
|
|
func readFile(path string) []byte {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return b
|
|
}
|
|
|
|
var users = map[int]string{}
|
|
|
|
func userName(uid int) string {
|
|
if n, ok := users[uid]; ok {
|
|
return n
|
|
}
|
|
n := strconv.Itoa(uid)
|
|
if u, err := user.LookupId(n); err == nil {
|
|
n = u.Username
|
|
}
|
|
users[uid] = n
|
|
return n
|
|
}
|