From ff7367cb2ef828502c3f7fde003f45d5daa33206 Mon Sep 17 00:00:00 2001 From: Kris Yotam <75515498+krisyotam@users.noreply.github.com> Date: Wed, 13 May 2026 01:05:09 -0500 Subject: [PATCH] add ram memory visualization script --- .local/bin/ram | 382 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 382 insertions(+) create mode 100755 .local/bin/ram diff --git a/.local/bin/ram b/.local/bin/ram new file mode 100755 index 0000000000000000000000000000000000000000..e41df459ab7961915c0b95531e4e8ba13e41259c --- /dev/null +++ b/.local/bin/ram @@ -0,0 +1,382 @@ +#!/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=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"