Skip to content

Instantly share code, notes, and snippets.

@kjanat
Last active April 11, 2026 16:59
Show Gist options
  • Select an option

  • Save kjanat/418e33723185e7efd09d4903901e4064 to your computer and use it in GitHub Desktop.

Select an option

Save kjanat/418e33723185e7efd09d4903901e4064 to your computer and use it in GitHub Desktop.
Grouped memory usage by application
#!/usr/bin/env bash
# memhogs — grouped memory usage by application
# Usage: memhogs [INTERVAL] [-w|--watch] [-k|--kill|--rescue]
# PRIORITY: rescue target must be available IMMEDIATELY.
# update_rescue runs FIRST, before flags, before anything.
# Every code path must have RESCUE_NAME set before any slow work or traps.
RESCUE_NAME=""
RESCUE_RSS=""
RESCUE_PROCS=""
TOTAL_MEM_KB=$(awk '/^MemTotal:/{print $2}' /proc/meminfo)
update_rescue() {
RESCUE_NAME="${1}"
RESCUE_RSS="${2}"
RESCUE_PROCS="${3:-}"
}
# THIS RUNS FIRST. Before arg parsing. Before anything.
# Fast path via ps — no /proc walk.
read -r fast_name fast_rss < <(ps -eo rss,comm --no-headers | awk '
{ rss[$2]+=$1 }
END { max=0; for(n in rss) if(rss[n]>max){max=rss[n]; name=n}; printf "%s %d", name, max/1024 }
' 2>/dev/null)
update_rescue "${fast_name}" "${fast_rss}"
unset fast_name fast_rss
MODE="once"
INTERVAL=3
# Parse flags
while [[ $# -gt 0 ]]; do
case "${1}" in
--internal-watch)
MODE="watch"
if [[ -n "${2:-}" ]] && [[ "${2}" =~ ^[0-9]+$ ]]; then
INTERVAL="${2}"
shift
fi
shift
;;
-w | --watch)
MODE="watch"
if [[ -n "${2:-}" ]] && [[ "${2}" =~ ^[0-9]+$ ]]; then
INTERVAL="${2}"
shift
fi
shift
;;
-k | --kill | --rescue)
# do_kill defined below, runs after trap is set
MODE="kill"
shift
;;
-h | --help)
echo "Usage: memhogs [INTERVAL] [-w|--watch] [-k|--kill|--rescue]"
echo ""
echo " (no args) Single snapshot"
echo " INTERVAL Watch with given interval (e.g. memhogs 6)"
echo " -w, --watch [N] Watch, optional interval (default: 3)"
echo " -k, --kill Kill the biggest memory hog (prompts first)"
echo ""
echo " WATCH=N memhogs Watch with interval N"
echo " WATCH= memhogs Watch with default interval"
exit 0
;;
[0-9]*)
MODE="watch"
INTERVAL="${1}"
shift
;;
*)
printf 'Unknown flag: %s\n' "${1}" >&2
exit 1
;;
esac
done
# WATCH env var: set = watch mode, numeric value = interval, empty = default
if [[ -n "${WATCH+set}" ]]; then
MODE="watch"
if [[ "${WATCH:-}" =~ ^[0-9]+$ ]] && [[ -n "${WATCH:-}" ]]; then
INTERVAL="${WATCH}"
fi
fi
# ---------------------------------------------------------------------------
# Data collection
# ---------------------------------------------------------------------------
collect_data() {
APP_TABLE=$(awk -v total="${TOTAL_MEM_KB}" '
BEGIN { OFS="\t" }
{
dir = "/proc/" $1
comm_file = dir "/comm"
status_file = dir "/status"
if ((getline name < comm_file) <= 0) next
close(comm_file)
rss = 0; swap = 0
while ((getline line < status_file) > 0) {
if (line ~ /^VmRSS:/) { split(line, a); rss = a[2] }
if (line ~ /^VmSwap:/) { split(line, a); swap = a[2] }
}
close(status_file)
rss_sum[name] += rss
swap_sum[name] += swap
count[name]++
}
END {
for (n in rss_sum) {
pct = (rss_sum[n] / total) * 100
if (rss_sum[n] > 1024)
printf "%s\t%d\t%d\t%d\t%.1f\n", n, rss_sum[n]/1024, swap_sum[n]/1024, count[n], pct
}
}' <(printf '%s\n' /proc/[0-9]* | sed 's|/proc/||') 2>/dev/null \
| sort -t$'\t' -k2 -rn | head -25)
SYSTEM_INFO=$(awk '
/^MemTotal:/ {mt=$2}
/^MemAvailable:/{ma=$2}
/^SwapTotal:/ {st=$2}
/^SwapFree:/ {sf=$2}
/^Zswap:/ {zs=$2}
/^Zswapped:/ {zw=$2}
END {
mu=mt-ma; su=st-sf
printf "RAM: %5dMB / %5dMB used (%5dMB avail)\n", mu/1024, mt/1024, ma/1024
printf "Swap: %5dMB / %5dMB used (%5dMB free)\n", su/1024, st/1024, sf/1024
if (zs > 0)
printf "Zswap: %5dMB pool → %5dMB data (%.1fx ratio)\n", zs/1024, zw/1024, zw/zs
}
' /proc/meminfo)
PSI_INFO=""
if [[ -f /proc/pressure/memory ]]; then
PSI_INFO=$(awk '
NR==1 {
for(i=1;i<=NF;i++) { split($i,kv,"="); v[kv[1]]=kv[2] }
printf "PSI: some avg10=%.2f%% avg60=%.2f%% avg300=%.2f%%\n", v["avg10"], v["avg60"], v["avg300"]
}
NR==2 {
for(i=1;i<=NF;i++) { split($i,kv,"="); v[kv[1]]=kv[2] }
printf " full avg10=%.2f%% avg60=%.2f%% avg300=%.2f%%\n", v["avg10"], v["avg60"], v["avg300"]
}
' /proc/pressure/memory)
fi
OOM_TABLE=$(
for dir in /proc/[0-9]*; do
score=$(<"${dir}/oom_score") || continue
[[ "${score}" -lt 10 ]] && continue
comm=$(<"${dir}/comm") || continue
rss=$(awk '/^VmRSS:/{printf "%dMB", $2/1024}' "${dir}/status" 2>/dev/null)
[[ -z "${rss}" ]] && continue
printf '%s\t%s\t%s\n' "${score}" "${rss}" "${comm}"
done 2>/dev/null | sort -t$'\t' -k1 -rn | head -10 \
| while IFS=$'\t' read -r score rss comm; do
printf '%-6s %8s %s\n' "${score}" "${rss}" "${comm}"
done
)
# Refine rescue target from full /proc data
local top_line
top_line=$(printf '%s\n' "${APP_TABLE}" | head -1)
update_rescue \
"$(printf '%s' "${top_line}" | cut -f1)" \
"$(printf '%s' "${top_line}" | cut -f2)" \
"$(printf '%s' "${top_line}" | cut -f4)"
# Snapshot for exec-based diff
CURRENT_DATA="${APP_TABLE}${SYSTEM_INFO}${PSI_INFO}${OOM_TABLE}${RESCUE_NAME}"
}
# ---------------------------------------------------------------------------
# Rendering
# ---------------------------------------------------------------------------
divider() {
local i
for ((i = 0; i < COLS; i++)); do printf '─'; done
}
section() {
local title="${1}"
local title_len=$((${#title} + 4))
local pad=$((COLS - title_len))
if [[ "${pad}" -lt 0 ]]; then pad=0; fi
printf '\e[1m── %s ' "${title}"
local i
for ((i = 0; i < pad; i++)); do printf '─'; done
printf '\e[0m'
}
rescue_line() {
printf 'pkill -x %s # frees ~%sMB' "${RESCUE_NAME}" "${RESCUE_RSS}"
}
render_frame() {
local term_width="${COLUMNS:-$(tput cols 2>/dev/null || echo 80)}"
COLS="${term_width}"
# Name column: fit the longest app name, minimum 16
local name_width=16
while IFS=$'\t' read -r n _; do
[[ ${#n} -gt ${name_width} ]] && name_width=${#n}
done <<<"${APP_TABLE}"
# Derive bar budget from terminal width minus fixed columns
local num_cols
num_cols=$(printf " %7dMB %7dMB %6d %5.1f%% " 0 0 0 0 | wc -m)
local fixed_cols=$((name_width + num_cols))
local bar_max=$((term_width - fixed_cols))
if [[ "${bar_max}" -lt 1 ]]; then bar_max=1; fi
# Top hog fills full bar, others scale relative
local top_rss
top_rss=$(printf '%s\n' "${APP_TABLE}" | head -1 | cut -f2)
top_rss="${top_rss:-1}"
local frame=""
frame+=$(printf "\e[1m%-${name_width}s %9s %9s %6s %6s %s\e[0m\n" \
"APPLICATION" "RSS" "SWAP" "PROCS" "MEM%" "BAR")
frame+=$'\n'"$(divider)"$'\n'
while IFS=$'\t' read -r name rss swap procs pct; do
[[ -z "${name}" ]] && continue
local bar_len color bar
bar_len=$(awk -v max="${bar_max}" -v top="${top_rss}" "BEGIN{v=int(${rss}/top*max); if(v>max)v=max; if(v<1 && ${rss}>0)v=1; print v}")
bar=""
local i
for ((i = 0; i < bar_len; i++)); do bar+='█'; done
if awk "BEGIN{exit !(${pct} > 10)}"; then
color='\e[31m'
elif awk "BEGIN{exit !(${pct} > 5)}"; then
color='\e[33m'
else
color='\e[32m'
fi
frame+=$(printf "%-${name_width}s %7dMB %7dMB %6d %5.1f%% ${color}%s\e[0m\n" \
"${name}" "${rss}" "${swap}" "${procs}" "${pct}" "${bar}")
frame+=$'\n'
done <<<"${APP_TABLE}"
frame+=$'\n'
frame+="$(section System)"$'\n'
frame+=$'\n'"${SYSTEM_INFO}"$'\n'
if [[ -n "${PSI_INFO}" ]]; then
frame+="${PSI_INFO}"$'\n'
fi
frame+=$'\n'
frame+="$(section "Top OOM kill targets")"$'\n'
frame+=$'\n'
frame+=$(printf '%-6s %8s %s\n' "SCORE" "RSS" "PROCESS")
frame+=$'\n'"$(divider)"$'\n'
frame+="${OOM_TABLE}"$'\n'
frame+=$'\n'
frame+="$(section "Emergency memory rescue")"$'\n'
frame+=$'\n'
frame+=$(printf '\e[33m%s\e[0m\n' "$(rescue_line)")
if [[ "${MODE}" == "watch" ]]; then
clear
fi
printf '%s\n' "${frame}"
}
# ---------------------------------------------------------------------------
# Actions
# ---------------------------------------------------------------------------
do_kill() {
if [[ -z "${RESCUE_NAME}" ]]; then
echo "No process found to kill." >&2
exit 1
fi
local detail=""
if [[ -n "${RESCUE_RSS}" ]]; then
detail=" (${RESCUE_RSS}MB"
if [[ -n "${RESCUE_PROCS}" ]]; then
detail+=" across ${RESCUE_PROCS} processes"
fi
detail+=")"
fi
printf 'Kill \e[1m%s\e[0m%s? [y/N] ' "${RESCUE_NAME}" "${detail}"
read -r confirm
if [[ "${confirm}" != "y" && "${confirm}" != "Y" ]]; then
echo "Aborted."
exit 0
fi
if pkill -x "${RESCUE_NAME}" 2>/dev/null; then
printf 'Killed %s\n' "${RESCUE_NAME}"
else
printf 'pkill failed (EPERM?), retrying with sudo...\n'
sudo pkill -x "${RESCUE_NAME}"
fi
}
on_exit() {
if [[ -n "${RESCUE_NAME}" ]]; then
echo ""
section "Emergency memory rescue"
echo ""
printf '\e[33m%s\e[0m\n' "$(rescue_line)"
fi
exit 0
}
# ---------------------------------------------------------------------------
# Main — one path, no branches
# ---------------------------------------------------------------------------
trap on_exit INT TERM
if [[ "${MODE}" == "kill" ]]; then
do_kill
exit 0
fi
collect_data
# In watch mode via exec: skip render if data unchanged
if [[ "${MODE}" == "watch" ]] && [[ "${CURRENT_DATA}" == "${MEMHOGS_PREV:-}" ]]; then
sleep "${INTERVAL}"
export MEMHOGS_PREV="${CURRENT_DATA}"
exec "$0" --internal-watch "${INTERVAL}"
fi
render_frame
[[ "${MODE}" == "once" ]] && exit 0
sleep "${INTERVAL}"
export MEMHOGS_PREV="${CURRENT_DATA}"
exec "$0" --internal-watch "${INTERVAL}"
#!/usr/bin/env bash
# memhogs2 — grouped memory usage by application
# Usage: memhogs2 [-w|--watch [INTERVAL]] [-k|--kill|--rescue]
RESCUE_CMD=""
RESCUE_NAME=""
TOTAL_MEM_KB=$(awk '/^MemTotal:/{print $2}' /proc/meminfo)
MODE="once"
INTERVAL=3
DO_KILL=false
PREV_FRAME=""
APP_TABLE=""
SYSTEM_INFO=""
PSI_INFO=""
OOM_TABLE=""
COLS=80
# --- terminal/style/glyph config (ASCII source, UTF-8 output when possible) ---
ESC=$'\033'
RESET="${ESC}[0m"
BOLD="${ESC}[1m"
DIM="${ESC}[2m"
RED="${ESC}[31m"
GREEN="${ESC}[32m"
YELLOW="${ESC}[33m"
CYAN="${ESC}[36m"
WHITE="${ESC}[37m"
is_utf8_locale() {
local loc="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"
[[ "${loc^^}" == *UTF-8* || "${loc^^}" == *UTF8* ]]
}
if is_utf8_locale; then
HR_CHAR=$'\342\224\200'
BAR_CHAR=$'\342\226\210'
else
HR_CHAR='-'
BAR_CHAR='#'
fi
show_help() {
cat <<'EOF'
Usage: memhogs2 [INTERVAL] [-w|--watch] [-k|--kill|--rescue]
(no args) Single snapshot
INTERVAL Watch with given interval (e.g. memhogs2 6)
-w, --watch [N] Watch, optional interval (default: 3)
-k, --kill Kill the biggest memory hog (prompts first)
WATCH=N memhogs2 Watch with interval N
WATCH= memhogs2 Watch with default interval
EOF
}
set_rescue_target() {
local name="${1:-}"
local rss_mb="${2:-0}"
local escaped=""
RESCUE_NAME="${name}"
if [[ -n "${name}" ]]; then
escaped=$(printf '%q' "${name}")
RESCUE_CMD="${RED}pkill -x --${RESET} ${CYAN}${escaped}${RESET} ${DIM}# frees ~${YELLOW}${rss_mb}MB${RESET}"
else
RESCUE_CMD="${DIM}No rescue target available${RESET}"
fi
}
calc_rescue_fast() {
local top_line name rss_mb
top_line=$(
ps -eo rss=,args= 2>/dev/null | awk '
{
cmd = $2
sub(/.*\//, "", cmd)
rss[cmd] += $1
}
END {
max = 0
for (name in rss) {
if (rss[name] > max) {
max = rss[name]
top = name
}
}
if (top != "")
printf "%s\t%d\n", top, max / 1024
}
'
)
if [[ -n "${top_line}" ]]; then
IFS=$'\t' read -r name rss_mb <<<"${top_line}"
set_rescue_target "${name}" "${rss_mb}"
else
set_rescue_target "" 0
fi
}
# Must run before arg parsing, traps, or any slower work.
calc_rescue_fast
parse_args() {
while [[ $# -gt 0 ]]; do
case "${1}" in
-w | --watch)
MODE="watch"
if [[ -n "${2:-}" && "${2}" =~ ^[0-9]+$ ]]; then
INTERVAL="${2}"
shift
fi
shift
;;
-k | --kill | --rescue)
DO_KILL=true
shift
;;
-h | --help)
show_help
exit 0
;;
[0-9]*)
MODE="watch"
INTERVAL="${1}"
shift
;;
*)
printf 'Unknown flag: %s\n' "${1}" >&2
exit 1
;;
esac
done
# WATCH env var:
# set => watch mode
# numeric => interval
# empty => default interval
if [[ -n "${WATCH+set}" ]]; then
MODE="watch"
if [[ -n "${WATCH:-}" && "${WATCH}" =~ ^[0-9]+$ ]]; then
INTERVAL="${WATCH}"
fi
fi
}
repeat_char() {
local count="${1:-0}"
local char="${2:--}"
local out=""
local i
((count < 1)) && return 0
for ((i = 0; i < count; i++)); do
out+="${char}"
done
printf '%s' "${out}"
}
divider() {
repeat_char "${COLS}" "${HR_CHAR}"
}
section() {
local title="${1}"
local title_len=$((${#title} + 4))
local pad=$((COLS - title_len))
((pad < 0)) && pad=0
printf '%s%s %s %s%s' "${BOLD}" "${HR_CHAR}${HR_CHAR}" "${title}" "$(repeat_char "${pad}" "${HR_CHAR}")" "${RESET}"
}
bar_chars() {
local len="${1:-0}"
((len < 1)) && len=1
repeat_char "${len}" "${BAR_CHAR}"
}
color_for_pct() {
local pct="${1:-0.0}"
local whole frac
whole="${pct%.*}"
frac="${pct#*.}"
if ((whole > 10 || (whole == 10 && frac > 0))); then
printf '%s' "${RED}"
elif ((whole > 5 || (whole == 5 && frac > 0))); then
printf '%s' "${YELLOW}"
else
printf '%s' "${GREEN}"
fi
}
update_rescue_from_table() {
local first name rss_mb
first=$(printf '%s\n' "${APP_TABLE}" | head -n1)
[[ -z "${first}" ]] && return
IFS=$'\t' read -r name rss_mb _ <<<"${first}"
set_rescue_target "${name}" "${rss_mb:-0}"
}
collect_app_table() {
APP_TABLE=$(
awk -v total="${TOTAL_MEM_KB}" '
BEGIN { OFS = "\t" }
{
dir = "/proc/" $1
comm_file = dir "/comm"
cmdline_file = dir "/cmdline"
status_file = dir "/status"
if ((getline name < comm_file) <= 0) next
close(comm_file)
# comm is kernel-truncated to 15 chars; try cmdline for full name
cmd = ""
if ((getline cmd < cmdline_file) > 0) {
gsub(/\0/, " ", cmd)
# strip leading path
n = split(cmd, parts, " ")
if (n > 0) {
sub(/.*\//, "", parts[1])
candidate = parts[1]
# use cmdline name if comm was truncated
if (length(name) >= 15 && length(candidate) > length(name))
name = candidate
}
}
close(cmdline_file)
rss = 0
swap = 0
while ((getline line < status_file) > 0) {
if (line ~ /^VmRSS:/) {
split(line, a)
rss = a[2]
} else if (line ~ /^VmSwap:/) {
split(line, a)
swap = a[2]
}
}
close(status_file)
rss_sum[name] += rss
swap_sum[name] += swap
count[name]++
}
END {
for (name in rss_sum) {
if (rss_sum[name] <= 1024) continue
pct = (rss_sum[name] / total) * 100
printf "%s\t%d\t%d\t%d\t%.1f\n",
name,
rss_sum[name] / 1024,
swap_sum[name] / 1024,
count[name],
pct
}
}
' < <(printf '%s\n' /proc/[0-9]* | sed 's|/proc/||') 2>/dev/null \
| sort -t$'\t' -k2,2rn \
| head -25
)
}
collect_system_info() {
# TSV: type \t used \t total \t extra_label \t extra_val \t note
SYSTEM_INFO=$(
awk '
/^MemTotal:/ { mt = $2 }
/^MemAvailable:/ { ma = $2 }
/^SwapTotal:/ { st = $2 }
/^SwapFree:/ { sf = $2 }
/^Zswap:/ { zs = $2 }
/^Zswapped:/ { zw = $2 }
END {
mu = mt - ma; su = st - sf
printf "RAM\t%d\t%d\t%d\tavail\n", mu/1024, mt/1024, ma/1024
printf "Swap\t%d\t%d\t%d\tfree\n", su/1024, st/1024, sf/1024
if (zs > 0)
printf "Zswap\t%d\t%d\t%.1f\tratio\n", zs/1024, zw/1024, zw/zs
}
' /proc/meminfo
)
}
collect_psi_info() {
PSI_INFO=""
[[ -f /proc/pressure/memory ]] || return
# TSV: type \t avg10 \t avg60 \t avg300
PSI_INFO=$(
awk '
{
for (i = 1; i <= NF; i++) {
split($i, kv, "=")
v[kv[1]] = kv[2]
}
label = (NR == 1) ? "some" : "full"
printf "%s\t%.2f%%\t%.2f%%\t%.2f%%\n", label, v["avg10"], v["avg60"], v["avg300"]
}
' /proc/pressure/memory
)
}
collect_oom_table() {
OOM_TABLE=$(
awk -v bold="${BOLD}" -v reset="${RESET}" -v yellow="${YELLOW}" '
{
dir = "/proc/" $1
score_file = dir "/oom_score"
adj_file = dir "/oom_score_adj"
comm_file = dir "/comm"
status_file = dir "/status"
cgroup_file = dir "/cgroup"
if ((getline score < score_file) <= 0) next
close(score_file)
score += 0
if (score < 10) next
adj = 0
if ((getline adj < adj_file) > 0) adj += 0
close(adj_file)
if ((getline name < comm_file) <= 0) next
close(comm_file)
rss = 0; swap = 0
while ((getline line < status_file) > 0) {
if (line ~ /^VmRSS:/) {
split(line, a); rss = a[2]
} else if (line ~ /^VmSwap:/) {
split(line, a); swap = a[2]
}
}
close(status_file)
if (rss < 1) next
# extract service/slice from cgroup path
slice = "?"
while ((getline line < cgroup_file) > 0) {
# cgroupv2: "0::/user.slice/user-1000.slice/..../foo.service"
n = split(line, parts, "/")
# walk segments, prefer .service, then .slice
for (si = n; si >= 1; si--) {
seg = parts[si]
if (seg ~ /\.service$/) { slice = seg; break }
}
if (slice == "?") {
for (si = n; si >= 1; si--) {
seg = parts[si]
if (seg ~ /\.slice$/ && seg != "-.slice") { slice = seg; break }
}
}
if (slice == "?") {
# fallback: innermost non-empty segment
for (si = n; si >= 1; si--) {
seg = parts[si]
sub(/\.scope$/, "", seg)
if (seg != "" && seg != "0::") { slice = seg; break }
}
}
}
close(cgroup_file)
pid = $1 + 0
count[name]++
rss_sum[name] += rss
swap_sum[name] += swap
# track slice + ppid from highest-scoring PID
if (score > max_score[name]) {
max_score[name] = score
slices[name] = slice
top_pid[name] = pid
}
if (adj != 0 && !(name in adj_val)) adj_val[name] = adj
# find parent pid
stat_file = dir "/stat"
if ((getline stat_line < stat_file) > 0) {
# /proc/PID/stat: pid (comm) state ppid ...
match(stat_line, /\) [A-Za-z] ([0-9]+)/, m)
if (m[1] + 0 > 1) {
ppid = m[1] + 0
if (!(name in parent_pid) || pid == top_pid[name])
parent_pid[name] = ppid
}
}
close(stat_file)
}
END {
for (name in max_score) {
adj_str = ""
if (name in adj_val) {
v = adj_val[name]
if (v < 0) adj_str = sprintf("adj %d", v)
else if (v > 0) adj_str = sprintf("adj +%d", v)
}
# prefer parent pid, fall back to top-scoring pid
target = (name in parent_pid) ? parent_pid[name] : top_pid[name]
printf "%d\t%d\t%d\t%d\t%s\t%s\t%s\t%d\n",
max_score[name],
rss_sum[name] / 1024,
swap_sum[name] / 1024,
count[name],
name,
slices[name],
adj_str,
target
}
}
' < <(printf '%s\n' /proc/[0-9]* | sed 's|/proc/||') 2>/dev/null \
| sort -t$'\t' -k1,1rn \
| head -10 \
| head -10
)
}
collect_data() {
collect_app_table
collect_system_info
collect_psi_info
collect_oom_table
update_rescue_from_table
}
render_frame() {
local frame=""
local term_width
local name_width=16
local num_cols
local fixed_cols
local bar_max
local top_rss
local sample
term_width="${COLUMNS:-$(tput cols 2>/dev/null || echo 80)}"
COLS="${term_width}"
while IFS=$'\t' read -r name _; do
((${#name} > name_width)) && name_width=${#name}
done <<<"${APP_TABLE}"
sample=$(printf " %7dMB %7dMB %6d %5.1f%% " 0 0 0 0)
num_cols=${#sample}
fixed_cols=$((name_width + num_cols))
bar_max=$((term_width - fixed_cols))
((bar_max < 1)) && bar_max=1
top_rss=$(printf '%s\n' "${APP_TABLE}" | head -n1 | cut -f2)
[[ -z "${top_rss}" || "${top_rss}" -lt 1 ]] && top_rss=1
frame+=$(printf "%s%-${name_width}s %9s %9s %6s %6s %s%s\n" \
"${BOLD}" "APPLICATION" "RSS" "SWAP" "PROCS" "MEM%" "BAR" "${RESET}")
frame+=$'\n'"$(divider)"$'\n'
while IFS=$'\t' read -r name rss swap procs pct; do
local bar_len color bar
[[ -z "${name}" ]] && continue
bar_len=$((rss * bar_max / top_rss))
((rss > 0 && bar_len < 1)) && bar_len=1
((bar_len > bar_max)) && bar_len=bar_max
bar=$(bar_chars "${bar_len}")
color=$(color_for_pct "${pct}")
frame+=$(printf "%s%-${name_width}s%s %s%7dMB %7dMB%s %s%6d%s %s%5.1f%%%s %s%s%s\n" \
"${CYAN}" "${name}" "${RESET}" \
"${YELLOW}" "${rss}" "${swap}" "${RESET}" \
"${DIM}" "${procs}" "${RESET}" \
"${WHITE}" "${pct}" "${RESET}" \
"${color}" "${bar}" "${RESET}")
frame+=$'\n'
done <<<"${APP_TABLE}"
frame+=$'\n'
frame+="$(section "System")"$'\n'
frame+=$'\n'
frame+=$(printf "%-6s %8s %8s %8s %s\n" "TYPE" "USED" "TOTAL" "OTHER" "NOTE")
frame+=$'\n'"$(divider)"$'\n'
while IFS=$'\t' read -r stype used total extra note; do
[[ -z "${stype}" ]] && continue
frame+=$(printf "%s%-6s%s %s%6dMB %6dMB %6sMB%s %s%s%s\n" \
"${CYAN}" "${stype}" "${RESET}" \
"${YELLOW}" "${used}" "${total}" "${extra}" "${RESET}" \
"${DIM}" "${note}" "${RESET}")
frame+=$'\n'
done <<<"${SYSTEM_INFO}"
if [[ -n "${PSI_INFO}" ]]; then
frame+=$'\n'
frame+="$(section "Memory pressure (PSI)")"$'\n'
frame+=$'\n'
frame+=$(printf "%-6s %8s %8s %8s\n" "TYPE" "AVG10" "AVG60" "AVG300")
frame+=$'\n'"$(divider)"$'\n'
while IFS=$'\t' read -r ptype avg10 avg60 avg300; do
[[ -z "${ptype}" ]] && continue
frame+=$(printf "%s%-6s%s %s%8s %8s %8s%s\n" \
"${CYAN}" "${ptype}" "${RESET}" \
"${YELLOW}" "${avg10}" "${avg60}" "${avg300}" "${RESET}")
frame+=$'\n'
done <<<"${PSI_INFO}"
fi
frame+=$'\n'
frame+="$(section "Top OOM kill targets")"$'\n'
frame+=$'\n'
# compute max widths for variable-length OOM columns
local oom_name_w=7 oom_adj_w=3 oom_slice_w=5 oom_kill_w=4
while IFS=$'\t' read -r _ _ _ _ name slice adj_note target_pid; do
[[ -z "${name}" ]] && continue
((${#name} > oom_name_w)) && oom_name_w=${#name}
((${#adj_note} > oom_adj_w)) && oom_adj_w=${#adj_note}
((${#slice} > oom_slice_w)) && oom_slice_w=${#slice}
local kc="kill ${target_pid}"
((${#kc} > oom_kill_w)) && oom_kill_w=${#kc}
done <<<"${OOM_TABLE}"
frame+=$(printf "%-6s %10s %10s %5s %-${oom_name_w}s %-${oom_adj_w}s %-${oom_slice_w}s %s\n" \
"SCORE" "RSS" "SWAP" "PROCS" "PROCESS" "ADJ" "SLICE" "KILL")
frame+=$'\n'"$(divider)"$'\n'
while IFS=$'\t' read -r score rss swap procs name slice adj_note target_pid; do
[[ -z "${score}" ]] && continue
kill_cmd="kill ${target_pid}"
frame+=$(printf "%s%-6s%s %s%8dMB %8dMB%s %s%5d%s %s%-${oom_name_w}s%s %s%-${oom_adj_w}s%s %s%-${oom_slice_w}s%s %s%s%s\n" \
"${WHITE}" "${score}" "${RESET}" \
"${YELLOW}" "${rss}" "${swap}" "${RESET}" \
"${DIM}" "${procs}" "${RESET}" \
"${CYAN}" "${name}" "${RESET}" \
"${DIM}" "${adj_note}" "${RESET}" \
"${GREEN}" "${slice}" "${RESET}" \
"${RED}" "${kill_cmd}" "${RESET}")
frame+=$'\n'
done <<<"${OOM_TABLE}"
frame+=$'\n'
frame+="$(section "Emergency memory rescue")"$'\n'
frame+=$'\n'
frame+=$(printf '%s\n' "${RESCUE_CMD}")
if [[ "${frame}" != "${PREV_FRAME}" ]]; then
[[ "${MODE}" == "watch" ]] && clear
printf '%s\n' "${frame}"
PREV_FRAME="${frame}"
fi
}
do_kill() {
if [[ -z "${RESCUE_NAME}" ]]; then
echo "No process found to kill." >&2
exit 1
fi
printf '%sKill%s %s%s%s? [y/N] ' "${RED}" "${RESET}" "${CYAN}" "${RESCUE_NAME}" "${RESET}"
read -r confirm
if [[ "${confirm}" != "y" && "${confirm}" != "Y" ]]; then
echo "Aborted."
exit 0
fi
if pkill -x -- "${RESCUE_NAME}" 2>/dev/null; then
printf 'Killed %s\n' "${RESCUE_NAME}"
else
printf 'pkill failed, retrying with sudo...\n'
sudo pkill -x -- "${RESCUE_NAME}"
fi
}
on_exit() {
if [[ -n "${RESCUE_CMD}" ]]; then
echo
section "Emergency memory rescue"
echo
printf '%s\n' "${RESCUE_CMD}"
fi
exit 0
}
run_once() {
collect_data
render_frame
}
run_watch() {
run_once
while true; do
sleep "${INTERVAL}"
collect_data
render_frame
done
}
main() {
parse_args "$@"
trap on_exit INT TERM
if "${DO_KILL}"; then
do_kill
exit 0
fi
if [[ "${MODE}" == "watch" ]]; then
run_watch
else
run_once
fi
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment