Initial Version
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# swaptop
|
||||
|
||||
htop-style view of who is using swap and how. Linux only.
|
||||
|
||||
go build -o swaptop . && sudo ./swaptop
|
||||
|
||||
Without root you only see full detail for your own processes, and the actions need root.
|
||||
|
||||
Header: swap bar split into green (owned by processes), yellow (swap cache: in RAM *and* holding a swap slot, i.e. reserved but not really swapped out) and magenta (tmpfs/shm, not owned by a process). zswap and zram figures appear when present.
|
||||
|
||||
Columns: SWAP (in swap now), SWPPSS (proportional share of shared pages), DELTA (change since last refresh), RSS, SWAPPED (share of the process that is in swap).
|
||||
|
||||
Keys: `Enter` per-mapping breakdown · `F7` swap a process back in · `F8` page a process out (`process_madvise`, Linux 5.10+) · `F2` flush every swap device (swapoff+swapon) · `F9` kill · `F3` `/` search · `F6` `s` sort · `z` show zero-swap processes · `c` full command line · `F1` help · `q` quit.
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
sysPidfdOpen = 434 // same number on every arch
|
||||
sysProcessMadvise = 440
|
||||
madvPageout = 21
|
||||
swapFlagPrefer = 0x8000
|
||||
swapFlagPrioMask = 0x7fff
|
||||
)
|
||||
|
||||
// swapIn faults every swapped-out page of pid back into RAM by reading it
|
||||
// through /proc/pid/mem. Pages are located via /proc/pid/pagemap so only
|
||||
// swapped pages are touched. Needs ptrace access to the target.
|
||||
func swapIn(pid int, progress func(doneKB, totalKB uint64)) error {
|
||||
maps, err := readSmaps(pid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir := "/proc/" + strconv.Itoa(pid)
|
||||
mem, err := os.Open(dir + "/mem")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer mem.Close()
|
||||
pm, err := os.Open(dir + "/pagemap")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer pm.Close()
|
||||
|
||||
page := uint64(os.Getpagesize())
|
||||
var total, done uint64
|
||||
for _, m := range maps {
|
||||
total += m.Swap
|
||||
}
|
||||
const chunk = 4096 // pagemap entries per read
|
||||
ents := make([]byte, chunk*8)
|
||||
buf := make([]byte, 256*page)
|
||||
swapped := func(i int) bool {
|
||||
e := binary.LittleEndian.Uint64(ents[i*8:])
|
||||
return e&(1<<62) != 0 && e&(1<<63) == 0 // in swap, not present
|
||||
}
|
||||
for _, m := range maps {
|
||||
if m.Swap == 0 {
|
||||
continue
|
||||
}
|
||||
if shmem(m) {
|
||||
// swapped shmem pages leave no swap PTE behind (the entry lives in the
|
||||
// page cache), so pagemap can't find them: just read the whole range.
|
||||
for addr := m.Start; addr < m.End; addr += uint64(len(buf)) {
|
||||
n := min(m.End-addr, uint64(len(buf)))
|
||||
mem.ReadAt(buf[:n], int64(addr))
|
||||
done += n / 1024
|
||||
progress(min(done, total), total)
|
||||
}
|
||||
continue
|
||||
}
|
||||
for addr := m.Start; addr < m.End; addr += chunk * page {
|
||||
n := min((m.End-addr)/page, chunk)
|
||||
got, err := pm.ReadAt(ents[:n*8], int64(addr/page*8))
|
||||
if got == 0 && err != nil {
|
||||
break
|
||||
}
|
||||
n = uint64(got / 8)
|
||||
for i := 0; i < int(n); {
|
||||
if !swapped(i) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
j := i + 1
|
||||
for j < int(n) && j-i < 256 && swapped(j) {
|
||||
j++
|
||||
}
|
||||
mem.ReadAt(buf[:uint64(j-i)*page], int64(addr+uint64(i)*page)) // errors (guard pages etc.) are fine
|
||||
done += uint64(j-i) * page / 1024
|
||||
progress(done, total)
|
||||
i = j
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// swapOut asks the kernel to reclaim pid's resident pages right now
|
||||
// (process_madvise MADV_PAGEOUT, Linux 5.10+). Anonymous pages go to swap,
|
||||
// clean file-backed pages are simply dropped. Returns bytes advised.
|
||||
func swapOut(pid int) (uint64, error) {
|
||||
maps, err := readSmaps(pid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
fd, _, e := syscall.Syscall(sysPidfdOpen, uintptr(pid), 0, 0)
|
||||
if e != 0 {
|
||||
return 0, fmt.Errorf("pidfd_open: %w", e)
|
||||
}
|
||||
defer syscall.Close(int(fd))
|
||||
var advised uint64
|
||||
var lastErr error
|
||||
for _, m := range maps {
|
||||
if m.Rss == 0 || skipPageout(m) {
|
||||
continue
|
||||
}
|
||||
iov := struct{ base, len uintptr }{uintptr(m.Start), uintptr(m.End - m.Start)}
|
||||
n, _, e := syscall.Syscall6(sysProcessMadvise, fd, uintptr(unsafe.Pointer(&iov)), 1, madvPageout, 0, 0)
|
||||
if e != 0 {
|
||||
lastErr = e
|
||||
continue
|
||||
}
|
||||
advised += uint64(n)
|
||||
}
|
||||
if advised == 0 && lastErr != nil {
|
||||
if errors.Is(lastErr, syscall.ENOSYS) {
|
||||
return 0, errors.New("process_madvise unsupported (needs Linux 5.10+)")
|
||||
}
|
||||
return 0, fmt.Errorf("process_madvise: %w", lastErr)
|
||||
}
|
||||
return advised, nil
|
||||
}
|
||||
|
||||
// ponytail: heuristic; a MAP_PRIVATE mapping of a tmpfs file is missed.
|
||||
func shmem(m Mapping) bool {
|
||||
if len(m.Perms) > 3 && m.Perms[3] == 's' {
|
||||
return true
|
||||
}
|
||||
for _, p := range []string{"/dev/zero", "/dev/shm/", "/memfd:", "/SYSV"} {
|
||||
if strings.HasPrefix(m.Path, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func skipPageout(m Mapping) bool {
|
||||
switch m.Path {
|
||||
case "[vsyscall]", "[vvar]", "[vdso]":
|
||||
return true
|
||||
}
|
||||
for _, f := range []string{" lo", " pf", " io", " ht"} { // locked, pfnmap, io, hugetlb
|
||||
if strings.Contains(" "+m.Flags+" ", f+" ") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// flushDev empties a swap device by swapoff+swapon, keeping its priority.
|
||||
// Needs root and enough free RAM to hold everything currently on the device.
|
||||
func flushDev(d SwapDev) error {
|
||||
p, err := syscall.BytePtrFromString(d.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, _, e := syscall.Syscall(syscall.SYS_SWAPOFF, uintptr(unsafe.Pointer(p)), 0, 0); e != 0 {
|
||||
return fmt.Errorf("swapoff %s: %w", d.Path, e)
|
||||
}
|
||||
flags := uintptr(0)
|
||||
if d.Prio >= 0 {
|
||||
flags = swapFlagPrefer | uintptr(d.Prio&swapFlagPrioMask)
|
||||
}
|
||||
if _, _, e := syscall.Syscall(syscall.SYS_SWAPON, uintptr(unsafe.Pointer(p)), flags, 0); e != 0 {
|
||||
return fmt.Errorf("swapon %s: %w (device is now OFF - re-enable manually)", d.Path, e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func permHint(err error) string {
|
||||
if errors.Is(err, os.ErrPermission) || errors.Is(err, syscall.EPERM) || errors.Is(err, syscall.EACCES) {
|
||||
return " (try: sudo swaptop)"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
module swaptop
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require github.com/gdamore/tcell/v2 v2.13.10
|
||||
|
||||
require (
|
||||
github.com/gdamore/encoding v1.0.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||
github.com/gdamore/tcell/v2 v2.13.10 h1:Afs3JKt83HnhuUKdZ3MnxUgOqQRWftj5JyDqv1LLynA=
|
||||
github.com/gdamore/tcell/v2 v2.13.10/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -0,0 +1,775 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
type view int
|
||||
|
||||
const (
|
||||
viewList view = iota
|
||||
viewDetail
|
||||
viewHelp
|
||||
)
|
||||
|
||||
var sortNames = []string{"SWAP", "SWPPSS", "DELTA", "RSS", "SWAPPED", "PID", "USER", "COMMAND"}
|
||||
|
||||
type modal struct {
|
||||
title string
|
||||
lines []string
|
||||
onYes func() // nil = info box
|
||||
}
|
||||
|
||||
type app struct {
|
||||
scr tcell.Screen
|
||||
sys *Sys
|
||||
prev map[int]uint64
|
||||
rows []Proc
|
||||
sel, top, dtop int
|
||||
sortBy int
|
||||
filter string
|
||||
searching bool
|
||||
hideZero bool
|
||||
fullCmd bool
|
||||
view view
|
||||
detail Proc
|
||||
maps []Mapping
|
||||
mapsErr error
|
||||
modal *modal
|
||||
busy bool
|
||||
|
||||
mu sync.Mutex
|
||||
status string
|
||||
}
|
||||
|
||||
var (
|
||||
stDefault = tcell.StyleDefault
|
||||
stHdr = tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorGreen)
|
||||
stSel = tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorTeal)
|
||||
stKey = tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorBlack)
|
||||
stFn = tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorTeal)
|
||||
stLabel = tcell.StyleDefault.Foreground(tcell.ColorTeal).Bold(true)
|
||||
stDim = tcell.StyleDefault.Foreground(tcell.ColorGray)
|
||||
stWarn = tcell.StyleDefault.Foreground(tcell.ColorYellow).Bold(true)
|
||||
stErr = tcell.StyleDefault.Foreground(tcell.ColorRed).Bold(true)
|
||||
stGreen = tcell.StyleDefault.Foreground(tcell.ColorGreen)
|
||||
stYellow = tcell.StyleDefault.Foreground(tcell.ColorYellow)
|
||||
stBlue = tcell.StyleDefault.Foreground(tcell.ColorBlue)
|
||||
stMagenta = tcell.StyleDefault.Foreground(tcell.ColorFuchsia)
|
||||
stRed = tcell.StyleDefault.Foreground(tcell.ColorRed)
|
||||
)
|
||||
|
||||
func main() {
|
||||
scr, err := tcell.NewScreen()
|
||||
if err == nil {
|
||||
err = scr.Init()
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "swaptop:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer scr.Fini()
|
||||
a := &app{scr: scr, hideZero: true}
|
||||
a.sample()
|
||||
events := make(chan tcell.Event, 64)
|
||||
go func() {
|
||||
for {
|
||||
ev := scr.PollEvent()
|
||||
if ev == nil {
|
||||
return
|
||||
}
|
||||
events <- ev
|
||||
}
|
||||
}()
|
||||
tick := time.NewTicker(2 * time.Second)
|
||||
for {
|
||||
a.draw()
|
||||
select {
|
||||
case ev := <-events:
|
||||
if a.handle(ev) {
|
||||
return
|
||||
}
|
||||
case <-tick.C:
|
||||
a.sample()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *app) sample() {
|
||||
a.sys = collect(a.prev)
|
||||
a.prev = make(map[int]uint64, len(a.sys.Procs))
|
||||
for _, p := range a.sys.Procs {
|
||||
a.prev[p.PID] = p.Swap
|
||||
}
|
||||
a.rebuild()
|
||||
if a.view == viewDetail {
|
||||
for _, p := range a.sys.Procs {
|
||||
if p.PID == a.detail.PID {
|
||||
a.detail = p
|
||||
}
|
||||
}
|
||||
a.maps, a.mapsErr = readSmaps(a.detail.PID)
|
||||
}
|
||||
}
|
||||
|
||||
func pct(p Proc) int {
|
||||
if p.Swap+p.RSS == 0 {
|
||||
return 0
|
||||
}
|
||||
return int(p.Swap * 100 / (p.Swap + p.RSS))
|
||||
}
|
||||
|
||||
func (a *app) rebuild() {
|
||||
selPID := -1
|
||||
if a.sel < len(a.rows) {
|
||||
selPID = a.rows[a.sel].PID
|
||||
}
|
||||
a.rows = a.rows[:0]
|
||||
f := strings.ToLower(a.filter)
|
||||
for _, p := range a.sys.Procs {
|
||||
if a.hideZero && p.Swap == 0 {
|
||||
continue
|
||||
}
|
||||
if f != "" && !strings.Contains(strings.ToLower(p.Cmd+" "+p.Comm+" "+p.User+" "+fmt.Sprint(p.PID)), f) {
|
||||
continue
|
||||
}
|
||||
a.rows = append(a.rows, p)
|
||||
}
|
||||
sort.SliceStable(a.rows, func(i, j int) bool {
|
||||
x, y := a.rows[i], a.rows[j]
|
||||
switch sortNames[a.sortBy] {
|
||||
case "SWPPSS":
|
||||
return x.SwapPss > y.SwapPss
|
||||
case "DELTA":
|
||||
return x.Delta > y.Delta
|
||||
case "RSS":
|
||||
return x.RSS > y.RSS
|
||||
case "SWAPPED":
|
||||
return pct(x) > pct(y)
|
||||
case "PID":
|
||||
return x.PID < y.PID
|
||||
case "USER":
|
||||
return x.User < y.User
|
||||
case "COMMAND":
|
||||
return x.Comm < y.Comm
|
||||
}
|
||||
return x.Swap > y.Swap
|
||||
})
|
||||
a.sel = 0
|
||||
for i, p := range a.rows {
|
||||
if p.PID == selPID {
|
||||
a.sel = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- input ----
|
||||
|
||||
func (a *app) handle(ev tcell.Event) (quit bool) {
|
||||
switch ev := ev.(type) {
|
||||
case *tcell.EventResize:
|
||||
a.scr.Sync()
|
||||
case *tcell.EventInterrupt:
|
||||
if ev.Data() == "done" {
|
||||
a.busy = false
|
||||
a.sample()
|
||||
}
|
||||
case *tcell.EventKey:
|
||||
return a.key(ev)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *app) key(ev *tcell.EventKey) bool {
|
||||
if ev.Key() == tcell.KeyCtrlC {
|
||||
return true
|
||||
}
|
||||
if m := a.modal; m != nil {
|
||||
switch {
|
||||
case m.onYes != nil && (ev.Rune() == 'y' || ev.Rune() == 'Y'):
|
||||
a.modal = nil
|
||||
m.onYes()
|
||||
case m.onYes == nil || ev.Key() == tcell.KeyEscape || ev.Rune() == 'n' || ev.Rune() == 'N' || ev.Rune() == 'q':
|
||||
a.modal = nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
if a.searching {
|
||||
switch ev.Key() {
|
||||
case tcell.KeyEscape:
|
||||
a.filter, a.searching = "", false
|
||||
case tcell.KeyEnter:
|
||||
a.searching = false
|
||||
case tcell.KeyBackspace, tcell.KeyBackspace2:
|
||||
if a.filter != "" {
|
||||
a.filter = a.filter[:len(a.filter)-1]
|
||||
}
|
||||
case tcell.KeyRune:
|
||||
a.filter += string(ev.Rune())
|
||||
}
|
||||
a.rebuild()
|
||||
return false
|
||||
}
|
||||
if a.view == viewHelp {
|
||||
a.view = viewList
|
||||
return false
|
||||
}
|
||||
n := len(a.rows)
|
||||
if a.view == viewDetail {
|
||||
n = len(a.maps)
|
||||
}
|
||||
pos := &a.sel
|
||||
if a.view == viewDetail {
|
||||
pos = &a.dtop
|
||||
}
|
||||
_, h := a.scr.Size()
|
||||
pg := max(h-10, 1)
|
||||
switch ev.Key() {
|
||||
case tcell.KeyUp:
|
||||
*pos--
|
||||
case tcell.KeyDown:
|
||||
*pos++
|
||||
case tcell.KeyPgUp:
|
||||
*pos -= pg
|
||||
case tcell.KeyPgDn:
|
||||
*pos += pg
|
||||
case tcell.KeyHome:
|
||||
*pos = 0
|
||||
case tcell.KeyEnd:
|
||||
*pos = n - 1
|
||||
case tcell.KeyEnter:
|
||||
if a.view == viewList && a.sel < len(a.rows) {
|
||||
a.detail, a.dtop = a.rows[a.sel], 0
|
||||
a.maps, a.mapsErr = readSmaps(a.detail.PID)
|
||||
a.view = viewDetail
|
||||
} else {
|
||||
a.view = viewList
|
||||
}
|
||||
case tcell.KeyEscape:
|
||||
a.view = viewList
|
||||
case tcell.KeyF1:
|
||||
a.view = viewHelp
|
||||
case tcell.KeyF2:
|
||||
a.askFlush()
|
||||
case tcell.KeyF3:
|
||||
a.searching = true
|
||||
case tcell.KeyF6:
|
||||
a.sortBy = (a.sortBy + 1) % len(sortNames)
|
||||
a.rebuild()
|
||||
case tcell.KeyF7:
|
||||
a.askSwapIn()
|
||||
case tcell.KeyF8:
|
||||
a.askSwapOut()
|
||||
case tcell.KeyF9:
|
||||
a.askKill()
|
||||
case tcell.KeyF10:
|
||||
return true
|
||||
case tcell.KeyRune:
|
||||
switch ev.Rune() {
|
||||
case 'q':
|
||||
if a.view == viewList {
|
||||
return true
|
||||
}
|
||||
a.view = viewList
|
||||
case '?', 'h':
|
||||
a.view = viewHelp
|
||||
case '/':
|
||||
a.searching = true
|
||||
case 's':
|
||||
a.sortBy = (a.sortBy + 1) % len(sortNames)
|
||||
a.rebuild()
|
||||
case 'z':
|
||||
a.hideZero = !a.hideZero
|
||||
a.rebuild()
|
||||
case 'c':
|
||||
a.fullCmd = !a.fullCmd
|
||||
case 'i':
|
||||
a.askSwapIn()
|
||||
case 'o':
|
||||
a.askSwapOut()
|
||||
case 'k':
|
||||
a.askKill()
|
||||
case 'F':
|
||||
a.askFlush()
|
||||
}
|
||||
}
|
||||
*pos = max(0, min(*pos, n-1))
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *app) current() (Proc, bool) {
|
||||
if a.view == viewDetail {
|
||||
return a.detail, true
|
||||
}
|
||||
if a.sel < len(a.rows) {
|
||||
return a.rows[a.sel], true
|
||||
}
|
||||
return Proc{}, false
|
||||
}
|
||||
|
||||
func (a *app) info(title string, lines ...string) { a.modal = &modal{title: title, lines: lines} }
|
||||
|
||||
func (a *app) runJob(fn func(set func(string)) error) {
|
||||
if a.busy {
|
||||
a.info("Busy", "Another action is still running.")
|
||||
return
|
||||
}
|
||||
a.busy = true
|
||||
set := func(s string) {
|
||||
a.mu.Lock()
|
||||
a.status = s
|
||||
a.mu.Unlock()
|
||||
a.scr.PostEvent(tcell.NewEventInterrupt(nil))
|
||||
}
|
||||
go func() {
|
||||
if err := fn(set); err != nil {
|
||||
set("Error: " + err.Error() + permHint(err))
|
||||
}
|
||||
a.scr.PostEvent(tcell.NewEventInterrupt("done"))
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *app) askSwapIn() {
|
||||
p, ok := a.current()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if p.Swap == 0 {
|
||||
a.info("Nothing to do", fmt.Sprintf("%s (%d) has no pages in swap.", p.Comm, p.PID))
|
||||
return
|
||||
}
|
||||
a.modal = &modal{title: "Swap in", lines: []string{
|
||||
fmt.Sprintf("Pull %s of %s (%d) back from swap into RAM?", human(p.Swap), p.Comm, p.PID),
|
||||
fmt.Sprintf("Available RAM: %s", human(a.sys.Mem["MemAvailable"])),
|
||||
}, onYes: func() {
|
||||
a.runJob(func(set func(string)) error {
|
||||
start, last := time.Now(), time.Time{}
|
||||
err := swapIn(p.PID, func(done, total uint64) {
|
||||
if time.Since(last) > 100*time.Millisecond {
|
||||
last = time.Now()
|
||||
set(fmt.Sprintf("Swapping in %s (%d): %s / %s", p.Comm, p.PID, human(done), human(total)))
|
||||
}
|
||||
})
|
||||
if err == nil {
|
||||
set(fmt.Sprintf("Swapped in %s (%d) in %s. Its slots stay 'cached' until the kernel drops them or you flush (F2).",
|
||||
p.Comm, p.PID, time.Since(start).Round(100*time.Millisecond)))
|
||||
}
|
||||
return err
|
||||
})
|
||||
}}
|
||||
}
|
||||
|
||||
func (a *app) askSwapOut() {
|
||||
p, ok := a.current()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
a.modal = &modal{title: "Swap out", lines: []string{
|
||||
fmt.Sprintf("Ask the kernel to page out %s of %s (%d) now?", human(p.RSS), p.Comm, p.PID),
|
||||
"Anonymous memory goes to swap, clean file pages are dropped.",
|
||||
"The process keeps running but will be slow until it faults back in.",
|
||||
}, onYes: func() {
|
||||
a.runJob(func(set func(string)) error {
|
||||
set(fmt.Sprintf("Swapping out %s (%d)...", p.Comm, p.PID))
|
||||
n, err := swapOut(p.PID)
|
||||
if err == nil {
|
||||
set(fmt.Sprintf("Advised kernel to page out %s of %s (%d).", human(n/1024), p.Comm, p.PID))
|
||||
}
|
||||
return err
|
||||
})
|
||||
}}
|
||||
}
|
||||
|
||||
func (a *app) askKill() {
|
||||
p, ok := a.current()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
a.modal = &modal{title: "Kill", lines: []string{fmt.Sprintf("Send SIGTERM to %s (%d)?", p.Comm, p.PID)}, onYes: func() {
|
||||
if err := syscall.Kill(p.PID, syscall.SIGTERM); err != nil {
|
||||
a.info("Kill failed", err.Error()+permHint(err))
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
func (a *app) askFlush() {
|
||||
used, avail := a.sys.SwapUsed(), a.sys.Mem["MemAvailable"]
|
||||
if len(a.sys.Devs) == 0 {
|
||||
a.info("No swap", "No active swap devices.")
|
||||
return
|
||||
}
|
||||
if os.Geteuid() != 0 {
|
||||
a.info("Need root", "Flushing a swap device (swapoff + swapon) needs root. Try: sudo swaptop")
|
||||
return
|
||||
}
|
||||
if used > avail {
|
||||
a.info("Not enough RAM", fmt.Sprintf("Swap holds %s but only %s RAM is available.", human(used), human(avail)),
|
||||
"Swap in / kill some processes first, or the OOM killer will decide for you.")
|
||||
return
|
||||
}
|
||||
lines := []string{fmt.Sprintf("swapoff + swapon every device, pulling %s back into RAM (%s available)?", human(used), human(avail))}
|
||||
for _, d := range a.sys.Devs {
|
||||
lines = append(lines, fmt.Sprintf(" %s used %s prio %d", d.Path, human(d.Used), d.Prio))
|
||||
}
|
||||
lines = append(lines, "Priority is preserved; discard/other swapon options are not.")
|
||||
devs := a.sys.Devs
|
||||
a.modal = &modal{title: "Flush swap", lines: lines, onYes: func() {
|
||||
a.runJob(func(set func(string)) error {
|
||||
start := time.Now()
|
||||
for _, d := range devs {
|
||||
set("Flushing " + d.Path + " (swapoff)...")
|
||||
if err := flushDev(d); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
set("Flushed all swap devices in " + time.Since(start).Round(100*time.Millisecond).String())
|
||||
return nil
|
||||
})
|
||||
}}
|
||||
}
|
||||
|
||||
// ---- drawing ----
|
||||
|
||||
func (a *app) put(x, y int, s string, st tcell.Style) int {
|
||||
w, _ := a.scr.Size()
|
||||
for _, r := range s {
|
||||
if x >= w {
|
||||
break
|
||||
}
|
||||
a.scr.SetContent(x, y, r, nil, st)
|
||||
x++
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func (a *app) fill(y int, st tcell.Style) {
|
||||
w, _ := a.scr.Size()
|
||||
for x := 0; x < w; x++ {
|
||||
a.scr.SetContent(x, y, ' ', nil, st)
|
||||
}
|
||||
}
|
||||
|
||||
type seg struct {
|
||||
frac float64
|
||||
st tcell.Style
|
||||
}
|
||||
|
||||
// bar draws an htop style Label[||||| text] of the given total width.
|
||||
func (a *app) bar(x, y, width int, label string, segs []seg, text string) {
|
||||
x = a.put(x, y, label, stLabel)
|
||||
x = a.put(x, y, "[", stDefault)
|
||||
inner := width - len(label) - 2
|
||||
if inner < 4 {
|
||||
return
|
||||
}
|
||||
pos, filled := 0, 0
|
||||
for _, s := range segs {
|
||||
n := int(s.frac*float64(inner) + 0.5)
|
||||
n = min(n, inner-filled)
|
||||
for i := 0; i < n; i++ {
|
||||
a.scr.SetContent(x+pos, y, '|', nil, s.st)
|
||||
pos++
|
||||
}
|
||||
filled += n
|
||||
}
|
||||
if len(text) < inner {
|
||||
a.put(x+inner-len(text), y, text, stDim)
|
||||
}
|
||||
a.put(x+inner, y, "]", stDefault)
|
||||
}
|
||||
|
||||
func (a *app) drawHeader(w int) int {
|
||||
m := a.sys.Mem
|
||||
total, used := m["SwapTotal"], a.sys.SwapUsed()
|
||||
procs := min(a.sys.ProcSwap, used)
|
||||
cached := min(m["SwapCached"], used-procs)
|
||||
other := used - procs - cached
|
||||
frac := func(v uint64) float64 {
|
||||
if total == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(v) / float64(total)
|
||||
}
|
||||
memTot := m["MemTotal"]
|
||||
memCache := m["Cached"] + m["SReclaimable"]
|
||||
memUsed := memTot - m["MemFree"] - m["Buffers"] - memCache
|
||||
mfrac := func(v uint64) float64 { return float64(v) / float64(max(memTot, 1)) }
|
||||
swapSegs := []seg{{frac(procs), stGreen}, {frac(cached), stYellow}, {frac(other), stMagenta}}
|
||||
memSegs := []seg{{mfrac(memUsed), stGreen}, {mfrac(m["Buffers"]), stBlue}, {mfrac(memCache), stYellow}}
|
||||
y := 0
|
||||
if w >= 100 {
|
||||
bw := (w - 2) / 2
|
||||
a.bar(1, y, bw, "Swp", swapSegs, human(used)+"/"+human(total))
|
||||
a.bar(1+bw+1, y, bw, "Mem", memSegs, human(memTot-m["MemAvailable"])+"/"+human(memTot))
|
||||
y++
|
||||
} else {
|
||||
a.bar(1, y, w-2, "Swp", swapSegs, human(used)+"/"+human(total))
|
||||
a.bar(1, y+1, w-2, "Mem", memSegs, human(memTot-m["MemAvailable"])+"/"+human(memTot))
|
||||
y += 2
|
||||
}
|
||||
x := a.put(1, y, "Swap used "+human(used)+": ", stDefault)
|
||||
x = a.put(x, y, "■ "+human(procs)+" in processes ", stGreen)
|
||||
x = a.put(x, y, "■ "+human(cached)+" cached (also in RAM, droppable) ", stYellow)
|
||||
x = a.put(x, y, "■ "+human(other)+" tmpfs/shm/unmapped ", stMagenta)
|
||||
a.put(x, y, "free "+human(total-used), stDim)
|
||||
y++
|
||||
if z := m["Zswapped"]; z > 0 {
|
||||
a.put(1, y, fmt.Sprintf("zswap: %s of that is held compressed in RAM (pool %s), the rest is on disk", human(z), human(m["Zswap"])), stDefault)
|
||||
y++
|
||||
}
|
||||
for _, d := range a.sys.Devs {
|
||||
p := 0
|
||||
if d.Size > 0 {
|
||||
p = int(d.Used * 100 / d.Size)
|
||||
}
|
||||
st := stDefault
|
||||
if p >= 90 {
|
||||
st = stRed
|
||||
}
|
||||
x := a.put(1, y, fmt.Sprintf("%-28s %-9s %8s used %s (%d%%) prio %d", d.Path, d.Type, human(d.Size), human(d.Used), p, d.Prio), st)
|
||||
if d.Zram != "" {
|
||||
a.put(x+2, y, d.Zram, stDim)
|
||||
}
|
||||
y++
|
||||
}
|
||||
line := fmt.Sprintf("Commit %s / %s %d of %d processes in swap sort: %s", human(m["Committed_AS"]), human(m["CommitLimit"]),
|
||||
a.sys.Swapped, len(a.sys.Procs), sortNames[a.sortBy])
|
||||
if a.hideZero {
|
||||
line += " (z: show all)"
|
||||
}
|
||||
if a.filter != "" {
|
||||
line += " filter: " + a.filter
|
||||
}
|
||||
x = a.put(1, y, line, stDefault)
|
||||
if a.sys.Unreadable > 0 {
|
||||
a.put(x+3, y, fmt.Sprintf("%d processes unreadable (not root)", a.sys.Unreadable), stWarn)
|
||||
}
|
||||
y++
|
||||
a.mu.Lock()
|
||||
status := a.status
|
||||
a.mu.Unlock()
|
||||
st := stWarn
|
||||
if strings.HasPrefix(status, "Error") {
|
||||
st = stErr
|
||||
}
|
||||
a.put(1, y, status, st)
|
||||
return y + 2
|
||||
}
|
||||
|
||||
func (a *app) draw() {
|
||||
a.scr.Clear()
|
||||
w, h := a.scr.Size()
|
||||
switch a.view {
|
||||
case viewHelp:
|
||||
a.drawHelp()
|
||||
case viewDetail:
|
||||
a.drawDetail(w, h)
|
||||
default:
|
||||
a.drawList(w, h)
|
||||
}
|
||||
a.drawBottom(w, h)
|
||||
if a.modal != nil {
|
||||
a.drawModal(w, h)
|
||||
}
|
||||
a.scr.Show()
|
||||
}
|
||||
|
||||
func (a *app) drawList(w, h int) {
|
||||
y := a.drawHeader(w)
|
||||
a.fill(y, stHdr)
|
||||
a.put(0, y, fmt.Sprintf("%7s %-10s %8s %8s %7s %8s %-13s %s", "PID", "USER", "SWAP", "SWPPSS", "DELTA", "RSS", "SWAPPED", "COMMAND"), stHdr)
|
||||
y++
|
||||
avail := h - 1 - y
|
||||
if avail < 1 {
|
||||
return
|
||||
}
|
||||
if a.sel < a.top {
|
||||
a.top = a.sel
|
||||
}
|
||||
if a.sel >= a.top+avail {
|
||||
a.top = a.sel - avail + 1
|
||||
}
|
||||
for i := a.top; i < len(a.rows) && i < a.top+avail; i++ {
|
||||
p := a.rows[i]
|
||||
st, bst, dst := stDefault, stGreen, stDefault
|
||||
if i == a.sel {
|
||||
a.fill(y, stSel)
|
||||
st, bst, dst = stSel, stSel, stSel
|
||||
} else {
|
||||
if pct(p) >= 75 {
|
||||
bst = stRed
|
||||
} else if pct(p) >= 25 {
|
||||
bst = stYellow
|
||||
}
|
||||
if p.Delta > 0 {
|
||||
dst = stRed
|
||||
} else if p.Delta < 0 {
|
||||
dst = stGreen
|
||||
}
|
||||
}
|
||||
pss := "-"
|
||||
if p.HasPss {
|
||||
pss = human(p.SwapPss)
|
||||
} else if p.Swap == 0 {
|
||||
pss = "0K"
|
||||
}
|
||||
x := a.put(0, y, fmt.Sprintf("%7d %-10.10s %8s %8s ", p.PID, p.User, human(p.Swap), pss), st)
|
||||
x = a.put(x, y, fmt.Sprintf("%7s", delta(p.Delta)), dst)
|
||||
x = a.put(x, y, fmt.Sprintf(" %8s ", human(p.RSS)), st)
|
||||
n := pct(p) * 8 / 100
|
||||
x = a.put(x, y, strings.Repeat("|", n), bst)
|
||||
x = a.put(x, y, strings.Repeat(" ", 8-n)+fmt.Sprintf("%3d%% ", pct(p)), st)
|
||||
cmd := p.Comm
|
||||
if a.fullCmd {
|
||||
cmd = p.Cmd
|
||||
}
|
||||
a.put(x, y, cmd, st)
|
||||
y++
|
||||
}
|
||||
}
|
||||
|
||||
func (a *app) drawDetail(w, h int) {
|
||||
p := a.detail
|
||||
a.put(1, 0, fmt.Sprintf("%s (%d) user %s", p.Comm, p.PID, p.User), stLabel)
|
||||
a.put(1, 1, fmt.Sprintf("Swap %s SwapPss %s RSS %s %d%% of its memory is in swap %d mappings",
|
||||
human(p.Swap), human(p.SwapPss), human(p.RSS), pct(p), len(a.maps)), stDefault)
|
||||
a.put(1, 2, p.Cmd, stDim)
|
||||
if a.mapsErr != nil {
|
||||
a.put(1, 3, "cannot read mappings: "+a.mapsErr.Error()+permHint(a.mapsErr), stErr)
|
||||
}
|
||||
y := 4
|
||||
a.fill(y, stHdr)
|
||||
a.put(0, y, fmt.Sprintf("%8s %8s %8s %-5s %-33s %s", "SWAP", "SWPPSS", "RSS", "PERM", "ADDRESS", "MAPPING"), stHdr)
|
||||
y++
|
||||
avail := h - 1 - y
|
||||
if avail < 1 {
|
||||
return
|
||||
}
|
||||
a.dtop = max(0, min(a.dtop, len(a.maps)-avail))
|
||||
for i := a.dtop; i < len(a.maps) && i < a.dtop+avail; i++ {
|
||||
m := a.maps[i]
|
||||
st := stDefault
|
||||
if m.Swap == 0 {
|
||||
st = stDim
|
||||
}
|
||||
path := m.Path
|
||||
if path == "" {
|
||||
path = "[anon]"
|
||||
}
|
||||
a.put(0, y, fmt.Sprintf("%8s %8s %8s %-5s %012x-%012x %s", human(m.Swap), human(m.SwapPss), human(m.Rss), m.Perms, m.Start, m.End, path), st)
|
||||
y++
|
||||
}
|
||||
}
|
||||
|
||||
func (a *app) drawHelp() {
|
||||
lines := []string{
|
||||
"swaptop - who is using swap, and how",
|
||||
"",
|
||||
"Columns",
|
||||
" SWAP swapped memory this process maps: its anonymous pages plus any shared memory (shmem/tmpfs) it has mapped",
|
||||
" SWPPSS its own share of anonymous swap (pages shared with other processes are split between them); shared memory is not counted",
|
||||
" DELTA change in SWAP since the last refresh (red = being swapped out, green = coming back in)",
|
||||
" RSS resident memory, in RAM",
|
||||
" SWAPPED how much of the process (SWAP+RSS) is in swap right now",
|
||||
"",
|
||||
"Swap bar",
|
||||
" green attributed to processes",
|
||||
" yellow swap cache: pages that are in RAM *and* still hold a swap slot (reserved, not really swapped out; dropped instantly under pressure)",
|
||||
" magenta not owned by any process: tmpfs/shm files, or slots whose owner we cannot read",
|
||||
"",
|
||||
"Actions (each asks for confirmation)",
|
||||
" F7 i swap in: fault every swapped page of the process back into RAM (reads /proc/PID/mem)",
|
||||
" F8 o swap out: ask the kernel to reclaim the process's pages now (process_madvise MADV_PAGEOUT, Linux 5.10+)",
|
||||
" F2 F flush: swapoff + swapon every device, emptying swap completely (root, needs enough free RAM)",
|
||||
" F9 k kill (SIGTERM)",
|
||||
"",
|
||||
"Keys",
|
||||
" Up/Down PgUp/PgDn Home/End move Enter per-mapping breakdown of the selected process",
|
||||
" F3 / search F6 s sort z show/hide processes with no swap c full command line F10 q quit",
|
||||
"",
|
||||
"Non-root: SWPPSS and actions only work for your own processes (kernel.yama.ptrace_scope).",
|
||||
"",
|
||||
"Press any key to return.",
|
||||
}
|
||||
for i, l := range lines {
|
||||
st := stDefault
|
||||
if i == 0 || (l != "" && l[0] != ' ') {
|
||||
st = stLabel
|
||||
}
|
||||
a.put(1, i, l, st)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *app) drawBottom(w, h int) {
|
||||
y := h - 1
|
||||
a.fill(y, stDefault)
|
||||
if a.searching {
|
||||
a.put(0, y, "Search: "+a.filter+"_", stWarn)
|
||||
return
|
||||
}
|
||||
keys := [][2]string{{"F1", "Help"}, {"F2", "Flush"}, {"F3", "Search"}, {"F6", "SortBy"}, {"F7", "SwapIn"}, {"F8", "SwapOut"}, {"F9", "Kill"}, {"F10", "Quit"}}
|
||||
if a.view != viewList {
|
||||
keys = [][2]string{{"Esc", "Back"}, {"F7", "SwapIn"}, {"F8", "SwapOut"}, {"F9", "Kill"}, {"F10", "Quit"}}
|
||||
}
|
||||
x := 0
|
||||
for _, k := range keys {
|
||||
x = a.put(x, y, fmt.Sprintf("%3s", k[0]), stKey)
|
||||
x = a.put(x, y, fmt.Sprintf("%-7s", k[1]), stFn)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *app) drawModal(w, h int) {
|
||||
m := a.modal
|
||||
lines := append([]string{}, m.lines...)
|
||||
if m.onYes != nil {
|
||||
lines = append(lines, "", "[y] yes [n] no")
|
||||
} else {
|
||||
lines = append(lines, "", "[Enter] ok")
|
||||
}
|
||||
bw := len(m.title)
|
||||
for _, l := range lines {
|
||||
bw = max(bw, len(l))
|
||||
}
|
||||
bw = min(bw+4, w)
|
||||
bh := len(lines) + 2
|
||||
x0, y0 := (w-bw)/2, (h-bh)/2
|
||||
st := tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorSilver)
|
||||
for y := y0; y < y0+bh; y++ {
|
||||
for x := x0; x < x0+bw; x++ {
|
||||
a.scr.SetContent(x, y, ' ', nil, st)
|
||||
}
|
||||
}
|
||||
a.put(x0+2, y0, m.title, st.Bold(true))
|
||||
for i, l := range lines {
|
||||
a.put(x0+2, y0+1+i, l, st)
|
||||
}
|
||||
}
|
||||
|
||||
func human(kb uint64) string {
|
||||
switch {
|
||||
case kb < 1024:
|
||||
return fmt.Sprintf("%dK", kb)
|
||||
case kb < 1024*1024:
|
||||
return fmt.Sprintf("%.1fM", float64(kb)/1024)
|
||||
default:
|
||||
return fmt.Sprintf("%.2fG", float64(kb)/1024/1024)
|
||||
}
|
||||
}
|
||||
|
||||
func delta(kb int64) string {
|
||||
switch {
|
||||
case kb > 0:
|
||||
return "+" + human(uint64(kb))
|
||||
case kb < 0:
|
||||
return "-" + human(uint64(-kb))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseSmaps(t *testing.T) {
|
||||
in := []byte(`7f00-7f10 rw-p 00000000 00:00 0 [heap]
|
||||
Rss: 100 kB
|
||||
Swap: 40 kB
|
||||
SwapPss: 20 kB
|
||||
VmFlags: rd wr mr mw me ac
|
||||
7f20-7f30 r--p 00000000 08:01 1234 /usr/lib/x.so (deleted)
|
||||
Rss: 8 kB
|
||||
Swap: 0 kB
|
||||
`)
|
||||
m := parseSmaps(in)
|
||||
if len(m) != 2 || m[0].Path != "[heap]" || m[0].Swap != 40 || m[0].SwapPss != 20 || m[0].Rss != 100 ||
|
||||
m[0].Start != 0x7f00 || m[0].End != 0x7f10 || m[0].Flags != "rd wr mr mw me ac" || m[1].Path != "/usr/lib/x.so (deleted)" {
|
||||
t.Fatalf("bad parse: %+v", m)
|
||||
}
|
||||
if parseKV([]byte("SwapTotal: 16 kB\nFoo: 3\n"))["SwapTotal"] != 16 {
|
||||
t.Fatal("parseKV")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user