#!/usr/bin/env bash
# ram — pretty visual breakdown of RAM + swap usage on this machine
# usage: ram [-n SIZE] [-p PAGE] [-a]
# -n N rows per page in process tables (default 15)
# -p N page number, 1-indexed (default 1)
# -a show all rows (disables pagination)
# header/breakdown sections always show; only process tables paginate
set -u
TOPN="${TOPN:-15}"
PAGE="${PAGE:-1}"
ALL=0
while [ $# -gt 0 ]; do
case "$1" in
-n) TOPN="$2"; shift 2 ;;
-p) PAGE="$2"; shift 2 ;;
-a|--all) ALL=1; shift ;;
-h|--help)
sed -n '2,7p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*) shift ;;
esac
done
# sanity
case "$TOPN" in (''|*[!0-9]*) TOPN=15 ;; esac
case "$PAGE" in (''|*[!0-9]*) PAGE=1 ;; esac
[ "$PAGE" -lt 1 ] && PAGE=1
[ "$TOPN" -lt 1 ] && TOPN=15
OFFSET=$(( (PAGE - 1) * TOPN ))
# ---- colors -----------------------------------------------------------------
if [ -t 1 ]; then
C_RESET=$'\033[0m'
C_BOLD=$'\033[1m'
C_DIM=$'\033[2m'
C_RED=$'\033[38;5;203m'
C_ORG=$'\033[38;5;215m'
C_YEL=$'\033[38;5;221m'
C_GRN=$'\033[38;5;114m'
C_CYN=$'\033[38;5;111m'
C_BLU=$'\033[38;5;75m'
C_MAG=$'\033[38;5;176m'
C_GRY=$'\033[38;5;244m'
else
C_RESET= C_BOLD= C_DIM= C_RED= C_ORG= C_YEL= C_GRN= C_CYN= C_BLU= C_MAG= C_GRY=
fi
# ---- helpers ----------------------------------------------------------------
human() {
# human-readable from KiB
python3 -c "
v=$1
u=['KiB','MiB','GiB','TiB']
i=0
while v>=1024 and i<len(u)-1:
v/=1024; i+=1
print(f'{v:.1f} {u[i]}')
"
}
bar() {
# bar percent width color
local pct="$1" width="${2:-40}" color="${3:-$C_GRN}"
python3 - "$pct" "$width" "$color" "$C_GRY" "$C_RESET" <<'PY'
import sys
pct=float(sys.argv[1]); w=int(sys.argv[2])
color=sys.argv[3]; gry=sys.argv[4]; rst=sys.argv[5]
filled=int(round(w*pct/100))
filled=max(0,min(w,filled))
print(f"{color}{'█'*filled}{gry}{'░'*(w-filled)}{rst}", end='')
PY
}
pct_color() {
local p="$1"
python3 -c "p=$p
print('"$C_RED"' if p>=90 else ('"$C_ORG"' if p>=75 else ('"$C_YEL"' if p>=50 else '"$C_GRN"')))"
}
hr() { printf "${C_GRY}%s${C_RESET}\n" "────────────────────────────────────────────────────────────────────────────────"; }
# ---- parse /proc/meminfo ----------------------------------------------------
declare -A MI
while IFS=':' read -r k v; do
v=$(echo "$v" | awk '{print $1}')
MI["$k"]="$v"
done < /proc/meminfo
MEM_TOTAL=${MI[MemTotal]}
MEM_FREE=${MI[MemFree]}
MEM_AVAIL=${MI[MemAvailable]}
BUFFERS=${MI[Buffers]:-0}
CACHED=${MI[Cached]:-0}
SRECLAIM=${MI[SReclaimable]:-0}
SHMEM=${MI[Shmem]:-0}
SLAB=${MI[Slab]:-0}
SUNRECLAIM=${MI[SUnreclaim]:-0}
KSTACK=${MI[KernelStack]:-0}
PAGETABLES=${MI[PageTables]:-0}
ANON=${MI[AnonPages]:-0}
MAPPED=${MI[Mapped]:-0}
SWAP_TOTAL=${MI[SwapTotal]:-0}
SWAP_FREE=${MI[SwapFree]:-0}
SWAP_CACHED=${MI[SwapCached]:-0}
DIRTY=${MI[Dirty]:-0}
# Effective "used" = total - available (matches what `free` reports as used)
MEM_USED=$(( MEM_TOTAL - MEM_AVAIL ))
SWAP_USED=$(( SWAP_TOTAL - SWAP_FREE ))
mem_pct=$(python3 -c "print(f'{$MEM_USED/$MEM_TOTAL*100:.1f}')")
swap_pct=$(python3 -c "t=$SWAP_TOTAL; print(f'{0 if t==0 else $SWAP_USED/t*100:.1f}')")
# ---- header -----------------------------------------------------------------
printf "\n ${C_BOLD}MEMORY${C_RESET} ${C_DIM}$(uname -n) — $(date '+%Y-%m-%d %H:%M:%S')${C_RESET}\n\n"
mc=$(pct_color "$mem_pct")
sc=$(pct_color "$swap_pct")
printf " ${C_BOLD}RAM ${C_RESET} "
bar "$mem_pct" 50 "$mc"
printf " ${mc}%5s%%${C_RESET} ${C_BOLD}%s${C_RESET} / %s ${C_DIM}avail %s${C_RESET}\n" \
"$mem_pct" "$(human $MEM_USED)" "$(human $MEM_TOTAL)" "$(human $MEM_AVAIL)"
printf " ${C_BOLD}SWAP${C_RESET} "
bar "$swap_pct" 50 "$sc"
printf " ${sc}%5s%%${C_RESET} ${C_BOLD}%s${C_RESET} / %s ${C_DIM}cached %s${C_RESET}\n" \
"$swap_pct" "$(human $SWAP_USED)" "$(human $SWAP_TOTAL)" "$(human $SWAP_CACHED)"
# ---- kernel / cache breakdown -----------------------------------------------
echo
printf " ${C_BOLD}Where it goes${C_RESET}\n"
hr
printf " %-22s %12s %s\n" "category" "size" "what it is"
hr
print_row() {
local label="$1" kib="$2" desc="$3" color="${4:-$C_CYN}"
printf " ${color}%-22s${C_RESET} %12s ${C_DIM}%s${C_RESET}\n" "$label" "$(human $kib)" "$desc"
}
print_row "AnonPages" "$ANON" "process heap/stack (the real working set)" "$C_RED"
print_row "Mapped" "$MAPPED" "files mmap'd into processes (libraries, etc)" "$C_ORG"
print_row "Shmem" "$SHMEM" "tmpfs + shared memory (/dev/shm, /tmp, IPC)" "$C_MAG"
print_row "Cached" "$CACHED" "page cache from disk reads (reclaimable)" "$C_GRN"
print_row "Buffers" "$BUFFERS" "block-device buffers (reclaimable)" "$C_GRN"
print_row "Slab" "$SLAB" "kernel object cache (dentries, inodes, …)" "$C_YEL"
print_row " ↳ SReclaim" "$SRECLAIM" "reclaimable portion of slab" "$C_GRN"
print_row " ↳ SUnreclaim" "$SUNRECLAIM" "unreclaimable slab — kernel actually needs it" "$C_RED"
print_row "KernelStack" "$KSTACK" "kernel stacks for live threads" "$C_CYN"
print_row "PageTables" "$PAGETABLES" "MMU page tables (grows with #procs * RSS)" "$C_CYN"
print_row "Dirty" "$DIRTY" "modified pages waiting to be written to disk" "$C_YEL"
# ---- /tmp + /dev/shm --------------------------------------------------------
echo
printf " ${C_BOLD}tmpfs occupancy${C_RESET} ${C_DIM}(lives in RAM — Shmem above includes this)${C_RESET}\n"
hr
python3 - <<'PY'
import subprocess
out = subprocess.run(['df','-k','--output=target,size,used,pcent','-t','tmpfs'],
capture_output=True, text=True).stdout.strip().splitlines()
def h(k):
v=float(k); u='K'
if v>=1024: v/=1024; u='M'
if v>=1024: v/=1024; u='G'
return f"{v:.1f}{u}"
rows=[]
for line in out[1:]:
parts=line.split()
if len(parts)<4: continue
target, size, used, pcent = parts[0], parts[1], parts[2], parts[3]
# filter trivial / credential mounts
if target.startswith('/run/credentials/'): continue
if int(used)==0: continue
rows.append((int(used), target, h(int(size)+0.0), h(int(used)+0.0), pcent))
rows.sort(reverse=True)
print(f" {'mount':<32} {'size':>8} {'used':>8} {'use%':>6}")
for _,t,s,u,p in rows:
print(f" {t:<32} {s:>8} {u:>8} {p:>6}")
PY
# ---- top processes by RSS ---------------------------------------------------
echo
printf " ${C_BOLD}Processes by RSS${C_RESET} ${C_DIM}(resident in RAM)${C_RESET}\n"
hr
printf " ${C_DIM}%-7s %-10s %9s %9s %6s %s${C_RESET}\n" "PID" "USER" "RSS" "VSZ" "%MEM" "COMMAND"
python3 - "$TOPN" "$OFFSET" "$ALL" "$C_RESET" "$C_DIM" "$C_RED" "$C_ORG" "$C_YEL" "$C_GRN" <<'PY'
import os, sys, pwd, subprocess
n=int(sys.argv[1]); off=int(sys.argv[2]); show_all=int(sys.argv[3])
rst=sys.argv[4]; dim=sys.argv[5]
r=sys.argv[6]; o=sys.argv[7]; y=sys.argv[8]; g=sys.argv[9]
def h(k):
v=float(k); u='K'
if v>=1024: v/=1024; u='M'
if v>=1024: v/=1024; u='G'
return f"{v:.1f}{u}"
out=subprocess.run(['ps','-eo','pid,user:12,rss,vsz,pmem,args','--sort=-rss','--no-headers'],
capture_output=True, text=True).stdout.splitlines()
total=len(out)
rows = out if show_all else out[off:off+n]
if not rows and off>0:
print(f" {dim}(no rows on this page — total {total}){rst}")
for line in rows:
parts=line.split(None, 5)
if len(parts)<6: continue
pid,user,rss,vsz,pmem,args=parts
pmemf=float(pmem)
col=g
if pmemf>=10: col=r
elif pmemf>=5: col=o
elif pmemf>=2: col=y
if len(args)>62: args=args[:59]+'...'
print(f" {pid:<7} {user:<10} {h(rss):>9} {h(vsz):>9} {col}{pmem:>5}%{rst} {args}")
if not show_all and total>n:
pages=(total+n-1)//n
cur=off//n + 1
lo=off+1; hi=min(off+n, total)
print(f" {dim}page {cur}/{pages} • rows {lo}-{hi} of {total}{rst}")
PY
# ---- top processes by swap --------------------------------------------------
echo
printf " ${C_BOLD}Processes by SWAP${C_RESET} ${C_DIM}(VmSwap from /proc — needs read perms)${C_RESET}\n"
hr
printf " ${C_DIM}%-7s %-10s %9s %9s %s${C_RESET}\n" "PID" "USER" "SWAP" "RSS" "COMMAND"
python3 - "$TOPN" "$OFFSET" "$ALL" "$C_DIM" "$C_RESET" <<'PY'
import os, sys, pwd
n=int(sys.argv[1]); off=int(sys.argv[2]); show_all=int(sys.argv[3])
dim=sys.argv[4]; rst=sys.argv[5]
rows=[]
for pid in os.listdir('/proc'):
if not pid.isdigit(): continue
try:
with open(f'/proc/{pid}/status') as f:
d={}
for line in f:
if ':' not in line: continue
k,v=line.split(':',1)
d[k]=v.strip()
sw=int(d.get('VmSwap','0 kB').split()[0])
rss=int(d.get('VmRSS','0 kB').split()[0])
if sw==0: continue
uid=int(d.get('Uid','0').split()[0])
try: user=pwd.getpwuid(uid).pw_name
except: user=str(uid)
name=d.get('Name','?')
try:
with open(f'/proc/{pid}/cmdline','rb') as f:
cmd=f.read().replace(b'\x00',b' ').decode('utf-8','replace').strip()
if not cmd: cmd=name
except: cmd=name
rows.append((sw,rss,pid,user,cmd))
except (FileNotFoundError, PermissionError, ProcessLookupError):
continue
rows.sort(reverse=True)
def h(k):
v=float(k); u='K'
if v>=1024: v/=1024; u='M'
if v>=1024: v/=1024; u='G'
return f"{v:.1f}{u}"
total=len(rows)
page = rows if show_all else rows[off:off+n]
if not page and off>0:
print(f" {dim}(no rows on this page — total {total}){rst}")
for sw,rss,pid,user,cmd in page:
if len(cmd)>62: cmd=cmd[:59]+'...'
print(f" {pid:<7} {user:<10} {h(sw):>9} {h(rss):>9} {cmd}")
if not rows:
print(" (no per-process swap data available — try with sudo)")
elif not show_all and total>n:
pages=(total+n-1)//n
cur=off//n + 1
lo=off+1; hi=min(off+n, total)
print(f" {dim}page {cur}/{pages} • rows {lo}-{hi} of {total}{rst}")
PY
# ---- per-user totals --------------------------------------------------------
echo
printf " ${C_BOLD}By user${C_RESET} ${C_DIM}(summed RSS / SWAP)${C_RESET}\n"
hr
python3 - "$TOPN" "$OFFSET" "$ALL" "$C_DIM" "$C_RESET" <<'PY'
import os, sys, pwd
n=int(sys.argv[1]); off=int(sys.argv[2]); show_all=int(sys.argv[3])
dim=sys.argv[4]; rst=sys.argv[5]
agg={}
for pid in os.listdir('/proc'):
if not pid.isdigit(): continue
try:
with open(f'/proc/{pid}/status') as f:
d={}
for line in f:
if ':' not in line: continue
k,v=line.split(':',1)
d[k]=v.strip()
rss=int(d.get('VmRSS','0 kB').split()[0])
sw=int(d.get('VmSwap','0 kB').split()[0])
uid=int(d.get('Uid','0').split()[0])
try: user=pwd.getpwuid(uid).pw_name
except: user=str(uid)
a=agg.setdefault(user,[0,0,0])
a[0]+=rss; a[1]+=sw; a[2]+=1
except (FileNotFoundError, PermissionError, ProcessLookupError):
continue
def h(k):
v=float(k); u='K'
if v>=1024: v/=1024; u='M'
if v>=1024: v/=1024; u='G'
return f"{v:.1f}{u}"
rows=sorted(agg.items(), key=lambda x:-x[1][0])
total=len(rows)
page = rows if show_all else rows[off:off+n]
print(f" {'user':<14} {'procs':>6} {'RSS':>10} {'SWAP':>10}")
if not page and off>0:
print(f" {dim}(no rows on this page — total {total}){rst}")
for u,(rss,sw,nproc) in page:
print(f" {u:<14} {nproc:>6} {h(rss):>10} {h(sw):>10}")
if not show_all and total>n:
pages=(total+n-1)//n
cur=off//n + 1
lo=off+1; hi=min(off+n, total)
print(f" {dim}page {cur}/{pages} • rows {lo}-{hi} of {total}{rst}")
PY
# ---- by process name (grouped, e.g. chrome) ---------------------------------
echo
printf " ${C_BOLD}Grouped by command${C_RESET} ${C_DIM}(same comm name summed — catches forky apps)${C_RESET}\n"
hr
python3 - "$TOPN" "$OFFSET" "$ALL" "$C_DIM" "$C_RESET" <<'PY'
import os, sys
n=int(sys.argv[1]); off=int(sys.argv[2]); show_all=int(sys.argv[3])
dim=sys.argv[4]; rst=sys.argv[5]
agg={}
for pid in os.listdir('/proc'):
if not pid.isdigit(): continue
try:
with open(f'/proc/{pid}/status') as f:
d={}
for line in f:
if ':' not in line: continue
k,v=line.split(':',1)
d[k]=v.strip()
rss=int(d.get('VmRSS','0 kB').split()[0])
sw=int(d.get('VmSwap','0 kB').split()[0])
name=d.get('Name','?')
if rss==0 and sw==0: continue
a=agg.setdefault(name,[0,0,0])
a[0]+=rss; a[1]+=sw; a[2]+=1
except (FileNotFoundError, PermissionError, ProcessLookupError):
continue
def h(k):
v=float(k); u='K'
if v>=1024: v/=1024; u='M'
if v>=1024: v/=1024; u='G'
return f"{v:.1f}{u}"
rows=sorted(agg.items(), key=lambda x:-(x[1][0]+x[1][1]))
total=len(rows)
page = rows if show_all else rows[off:off+n]
print(f" {'command':<28} {'procs':>6} {'RSS':>10} {'SWAP':>10}")
if not page and off>0:
print(f" {dim}(no rows on this page — total {total}){rst}")
for name,(rss,sw,np) in page:
print(f" {name[:28]:<28} {np:>6} {h(rss):>10} {h(sw):>10}")
if not show_all and total>n:
pages=(total+n-1)//n
cur=off//n + 1
lo=off+1; hi=min(off+n, total)
print(f" {dim}page {cur}/{pages} • rows {lo}-{hi} of {total}{rst}")
PY
echo
printf " ${C_DIM}tip: ${C_RESET}${C_DIM}ram -n SIZE -p PAGE • ram -a shows all rows • sudo ram reads every process${C_RESET}\n\n"