|
############################################################################### |
|
# 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' |
|
} |