Initial Version
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
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, UID int
|
||||
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 "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
|
||||
}
|
||||
Reference in New Issue
Block a user