Files
Andy Dixon ccbbd416b7 swaptop 1.0.0
htop-style view of which processes are using swap, and how.
2026-10-07 12:25:29 +01:00

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
}