Skip to content

Instantly share code, notes, and snippets.

@Koriit
Last active June 26, 2025 21:13
Show Gist options
  • Save Koriit/1c78e6b4596aff3368284d15914d5526 to your computer and use it in GitHub Desktop.
Save Koriit/1c78e6b4596aff3368284d15914d5526 to your computer and use it in GitHub Desktop.
cpugraph alpine

cpugraph for alpine

A 100% BusyBox-friendly TUI that shows CPU utilisation and Linux cgroup throttling in real time. Just awk, ANSI, and a sprinkle of unicode.

cpugraph --as-cpu-cores --pid 1 --throttling-graph
459613574 83898871 a3a2 4703 9037 7caea12da76d.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NTA5NzAxMzgsIm5iZiI6MTc1MDk2OTgzOCwicGF0aCI6Ii84OTE2MzkzLzQ1OTYxMzU3NC04Mzg5ODg3MS1hM2EyLTQ3MDMtOTAzNy03Y2FlYTEyZGE3NmQucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDYyNiUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTA2MjZUMjAzMDM4WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9OGViMjNmOGI5Yjc5MWFkYzg5YTU5MWM4ODUxNjBhZTg0NTFkMDVkMmM5MTRjNDg2ZmUzZjg4YjdkZjg0YzM5YSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ

Why does this exist 🤔 ?

  • Docker & Kubernetes love Alpine - but BusyBox lacks fancy monitors.

  • Want immediate visual feedback inside minimal containers where you can’t apk add htop.

  • Needed throttling insight (∆nr_throttled / ∆nr_periods) while load-testing on saturated hosts.

  • Such containers often also use read-only filesystems so you cannot just create a script ("but you still have your console, Harry").

  • Let’s be honest, human brains in general are not made for analysing trends and patterns when given inputs one at a time as top gives us.

    • Note that the goal is not to replace top, or htop, or any such tool.

Blame long nights, too much coffee, and ChatGPT o3 – our AI colleague who never got bored of shell arithmetic loops.

What it does

  • Upper graph → CPU utilisation

    • Default scale 0-100%

    • --as-cpu-cores → scaled to logical core units, useful when comparing with K8s metrics

    • Bars turn red on samples that were throttled

  • Optional lower graph (--throttling-graph) → % of scheduler periods throttled

  • Supports per-process view via --pid <N> (e.g. --pid 1 inside a container)

  • Works in read-only root filesystems - zero disk writes

Installation

Just drop the function right into the active sh prompt.

Usually you can access it with:

kubectl exec -it YOUR_POD -- sh

Usage

cpugraph -h                    # access help text
cpugraph                       # whole-CPU, percent scale
cpugraph --as-cpu-cores        # show as used cores (0 - N)
cpugraph --pid 2451            # monitor single PID
cpugraph --throttling-graph    # add throttling history
cpugraph --interval 2 --width 80 --height 24

Hit Ctrl-C to exit; your original terminal state is restored thanks to the alternate-screen buffer trick.

###############################################################################
# cpugraph
# ---------------------------------------------------------------------------
# A pure-shell, real-time CPU-utilisation & cgroup-throttling visualiser that
# runs even in tiny BusyBox/Alpine containers.
###############################################################################
cpugraph() {
###########################################################################
# 0  HELP TEXT
###########################################################################
USAGE='cpugraph – real-time CPU & throttling visualiser
Usage: cpugraph [options] (Ctrl-C to quit)
--interval N seconds per sample [1]
--width N history length (columns) [120]
--height N utilisation rows [20]
--as-cpu-cores y-axis in logical-core units
--pid N track single process (PID)
--throttling-graph add %-throttled history graph
-h | --help show this help
'
[ "$1" = "-h" ] || [ "$1" = "--help" ] && { printf "%s" "$USAGE"; return 0; }
###########################################################################
# 1  CLI PARSING – accept flags in any order
###########################################################################
INTERVAL=1 WIDTH=120 HEIGHT=20
AS_CORES=0 PID="" THR_GRAPH=0
while [ $# -gt 0 ]; do
case "$1" in
--interval) shift; INTERVAL=${1:-1} ;;
--width) shift; WIDTH=${1:-120} ;;
--height) shift; HEIGHT=${1:-20} ;;
--as-cpu-cores) AS_CORES=1 ;;
--pid) shift; PID=$1 ;;
--throttling-graph) THR_GRAPH=1 ;;
*) printf >&2 "cpugraph: unknown option '%s' (see --help)\n" "$1"; return 1 ;;
esac; shift
done
[ -n "$PID" ] && [ ! -r "/proc/$PID/stat" ] && { echo "cpugraph: PID $PID not found"; return 1; }
###########################################################################
# 2  CONSTANTS & ONE-OFF CALCULATIONS
###########################################################################
TH_HEIGHT=$((HEIGHT/2)) # rows for throttling sub-graph
BAR='' UNSAMPLED='.' # glyphs for filled / empty dots
RED='\033[31m' NORM='\033[0m' # ANSI for red & reset
CORES=$(awk '/^processor/{c++} END{print c?c:1}' /proc/cpuinfo)
STATIC_HDR="CONF │ interval=${INTERVAL}s width=${WIDTH} height=${HEIGHT} \
scale=$( [ $AS_CORES -eq 1 ] && echo cores || echo % ) pid=${PID:-all} \
throttlingGraph=${THR_GRAPH}"
###########################################################################
# 3  RING BUFFERS – history, raw util values, throttle %
###########################################################################
history="" util_raw="" thr_hist=""
for _ in $(seq 1 "$WIDTH"); do
history="$history -1:0" # "value:flag" (flag=1 if throttled)
util_raw="$util_raw -1"
thr_hist="$thr_hist -1"
done
# trim leading space from each buffer
history=${history# } util_raw=${util_raw# } thr_hist=${thr_hist# }
###########################################################################
# 4  HELPER ONE-LINER FUNCTIONS (single awk each)
###########################################################################
get_sys() { awk '/^cpu /{u=$2+$3+$4+$6+$7+$8; t=u+$5+$9+$10; print u,t}' /proc/stat; }
get_proc() { [ -r "/proc/$PID/stat" ] && awk '{print $14+$15}' "/proc/$PID/stat" || echo 0; }
get_cg() { awk '/nr_periods/ {p=$2} /nr_throttled/ {t=$2} END{print p,t}' \
/sys/fs/cgroup/cpu.stat 2>/dev/null; }
# baseline counters (monotonic growing numbers)
set -- $(get_sys); prev_u=$1 prev_tot=$2
prev_p=$(get_proc)
set -- $(get_cg); prev_per=$1 prev_thr=$2
###########################################################################
# 5  TERMINAL PREP : alt-buffer + hide cursor
###########################################################################
printf '\033[?1049h\033[H\033[?25l'
trap 'finished=1' INT # Ctrl-C sets flag → loop exits
clearln() { printf '\033[2K%b\n' "$1"; } # erase line, print with ANSI interpreted
###########################################################################
# 6  MAIN REFRESH LOOP
###########################################################################
finished=0
while [ "$finished" -eq 0 ]; do
#######################################################################
# 6.1 — SAMPLE (util %, throttle %)
#######################################################################
set -- $(get_sys); cur_u=$1 cur_tot=$2
du=$((cur_u-prev_u)); dt=$((cur_tot-prev_tot)); prev_u=$cur_u; prev_tot=$cur_tot
if [ -n "$PID" ]; then
cur_p=$(get_proc); dp=$((cur_p-prev_p)); prev_p=$cur_p
fi
if [ -z "$dt" ] || [ "$dt" -eq 0 ]; then
pct=0
else
pct=$((100*du/dt))
fi
if [ "$pct" -lt 0 ]; then pct=0; fi
if [ "$pct" -gt 100 ]; then pct=100; fi
set -- $(get_cg); cur_per=$1 cur_thr=$2
d_per=$((cur_per-prev_per)); d_thr=$((cur_thr-prev_thr))
prev_per=$cur_per; prev_thr=$cur_thr
if [ -z "$d_per" ] || [ "$d_per" -eq 0 ]; then
thr_pct=0
else
thr_pct=$((100*d_thr/d_per))
fi
throttled=$([ "$d_thr" -gt 0 ] && echo 1 || echo 0)
#######################################################################
# 6.2 — MAP util % → bar-height units (0-HEIGHT)
#######################################################################
if [ $AS_CORES -eq 1 ]; then
usedC=$(awk -v p=$pct -v c=$CORES 'BEGIN{v=p*c/100; if(v>c)v=c; printf "%.2f", v}')
util_units=$(awk -v uc=$usedC -v c=$CORES -v h=$HEIGHT 'BEGIN{print int(uc*h/c+0.5)}')
util_val=$usedC
else
util_units=$pct; util_val=$pct
fi
# slide ring buffers
history="${history#* }"; history="$history $util_units:$throttled"
util_raw="${util_raw#* }"; util_raw="$util_raw $util_val"
thr_hist="${thr_hist#* }"; thr_hist="$thr_hist $thr_pct"
#######################################################################
# 6.3 — STATS (min / max / avg for window)
#######################################################################
eval "$(echo "$util_raw" | awk '
BEGIN{mn=1e9;mx=-1;sm=0;n=0}
{for(i=1;i<=NF;i++){v=$i;if(v>=0){mn=(v<mn?v:mn);mx=(v>mx?v:mx);sm+=v;n++}}}
END{if(n)printf "umin=%.1f umax=%.1f uavg=%.1f\n",mn,mx,sm/n; else print "umin=- umax=- uavg=-"}')"
eval "$(echo "$thr_hist" | awk '
BEGIN{mn=1e9;mx=-1;sm=0;n=0}
{for(i=1;i<=NF;i++){v=$i;if(v>=0){mn=(v<mn?v:mn);mx=(v>mx?v:mx);sm+=v;n++}}}
END{if(n)printf "tmin=%d tmax=%d tavg=%.1f\n",mn,mx,sm/n; else print "tmin=- tmax=- tavg=-"}')"
#######################################################################
# 6.4 — DRAW FRAME
#######################################################################
printf '\033[H' # home cursor
clearln "$STATIC_HDR"
samples=$(printf '%s\n' $history | grep -vc '^-1')
DYN="LIVE │ samples=${samples}/${WIDTH} usage=${pct}% throttling=${thr_pct}%"
[ $AS_CORES -eq 1 ] && DYN="$DYN cores=${usedC}/${CORES}"
clearln "$DYN"
clearln "$(printf 'STAT │ usage min=%s max=%s avg=%s %s │ throttling min=%s%% max=%s%% avg=%s%%' \
"$umin" "$umax" "$uavg" "$( [ $AS_CORES -eq 1 ] && echo cores || echo '%')" \
"$tmin" "$tmax" "$tavg")"
echo
###################################################################
# draw utilisation graph body (HEIGHT rows, newest sample at right)
###################################################################
echo '=== CPU UTILISATION ==='
for row in $(seq "$HEIGHT" -1 1); do
lbl=$( [ $AS_CORES -eq 1 ] \
&& awk -v r=$row -v c=$CORES -v h=$HEIGHT 'BEGIN{printf "%5.2f", r*c/h}' \
|| printf "%3d" $((row*100/HEIGHT)) )
line=
for tok in $history; do
val=${tok%:*}; flg=${tok#*:}
if [ $val -lt 0 ]; then char=$UNSAMPLED
else
if [ $AS_CORES -eq 1 ]; then [ $val -ge $row ] && char=$BAR || char=' '
else bu=$((val*HEIGHT/100)); [ $bu -ge $row ] && char=$BAR || char=' '
fi
# colour filled bar if throttled
[ "$char" = "$BAR" ] && [ $flg -eq 1 ] && char="${RED}${BAR}${NORM}"
fi
line="${line}${char}"
done
clearln "$line $( [ $AS_CORES -eq 1 ] && echo "${lbl} cores" || echo "${lbl}%")"
done
printf '%0.s─' $(seq 1 "$WIDTH"); [ $AS_CORES -eq 1 ] && printf ' %5.2f cores\n' 0 || printf ' %3d%%\n' 0
###################################################################
# X-AXIS (seconds-ago, right-aligned to newest sample)
###################################################################
TICK=$((WIDTH/8)); [ $TICK -lt 5 ] && TICK=5
axis=$(awk -v W=$WIDTH -v T=$TICK -v I=$INTERVAL '
BEGIN{
for(i=0;i<W;i++)x[i]=" ";
for(col=W-1; col>=0; col-=T){
s=sprintf("%d",(W-1-col)*I);
for(j=length(s)-1,k=col; j>=0 && k>=0; j--,k--)
x[k]=substr(s,j+1,1);
}
for(i=0;i<W;i++)printf "%s",x[i];
}')
clearln "$axis s ago"
###################################################################
# throttling graph (optional)
###################################################################
if [ $THR_GRAPH -eq 1 ]; then
echo; clearln '=== THROTTLING (% periods) ==='
for row in $(seq "$TH_HEIGHT" -1 1); do
lbl=$(printf "%3d" $((row*100/TH_HEIGHT)))
line=
for v in $thr_hist; do
if [ $v -lt 0 ]; then line="${line}${UNSAMPLED}"
else bu=$((v*TH_HEIGHT/100)); [ $bu -ge $row ] && line="${line}${BAR}" || line="${line} "
fi
done
clearln "$line ${lbl}%"
done
printf '%0.s─' $(seq 1 "$WIDTH"); printf ' %3d%%\n' 0
clearln "$axis s ago"
fi
sleep "$INTERVAL"
done
printf '\033[?25h\033[?1049l'
}
@Koriit
Copy link
Author

Koriit commented Jun 26, 2025

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment