Skip to content

Instantly share code, notes, and snippets.

@nuumio
Last active April 19, 2020 13:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nuumio/574ac198a7f33db9e459106d0499c8da to your computer and use it in GitHub Desktop.
Save nuumio/574ac198a7f33db9e459106d0499c8da to your computer and use it in GitHub Desktop.
Disk stat watcher
#!/bin/bash
# Update interval
INT=0.1
# "Sample count" for speed calculations (for averaging)
SC=20
FROMBOOT=false
#for i in "$@"; do
while [[ $# -gt 0 ]]; do
case "$1" in
-i|--interval)
INT="$2"
echo "INT=${INT}"
shift
shift
;;
-s|--samples)
SC="$2"
echo "SC=${SC}"
shift
shift
;;
-b|--boot)
# START from boot time
FROMBOOT=true
shift
;;
-r|--runtime)
# RUNTIME only (ie. time span from START to NOW)
TF="RUN"
shift
;;
--fulltime)
# From START to NOW for RUNTIME
TF="FULL"
shift
;;
-f|--fromstart)
# From START for RUNTIME
TF="FROMSTART"
shift
;;
*)
echo "Invalid option: ${i}"
exit 1
;;
esac
done
# Non-breaking space
NBSP="\xC2\xA0"
# "Buffers" for storing disk data
NB=$((SC + 1))
LAST=$((NB - 1))
# Need gawk for big numbers
AWK="gawk --bignum"
# Sectors (512 bytes) to GiB and MiB
S2G="512 / 1073741824"
S2M="512 / 1048576"
# Nanos in s
NS=1000000000
# awk raw stat print
# Output: device/Total, sectors read, sectors written, time in seconds
PRAW="print \$3, \$6, \$10, now"
PRAWTOT="print \"Total\", tsr, tsw, now"
# awk printf for calculated values
# Input:
# device, NEWEST sectors read, NEWEST sectors written, NEWEST time in seconds, device, OLDEST sectors read, OLDEST sectors written, OLDEST time in seconds
# Output (+erase to end of line):
# dev, sectors read, sectors written, GiBs read, GiBs writted, GiBs xferred, read speed, write speed, xfer speed
FMTCALC="%s %d %d %.2f %.2f %.2f %.2f %.2f %.2f\033[0K\n"
PCALC="printf(\"${FMTCALC}\", \$1, \$2, \$3, \
\$2 * ${S2G}, \$3 * ${S2G}, (\$2 + \$3) * ${S2G}, \
(\$2 - \$6) * ${S2M} / dt, (\$3 - \$7) * ${S2M} / dt, ((\$2 - \$6) + (\$3 - \$7)) * ${S2M} / dt)"
# awk relative stats print
# Output: device, sectors read, sectors written, time in seconds
PREL="print \$1, \$2 - \$6, \$3 - \$7, \$4 - \$8"
# Print device, sectors read, sectors written for devices and total
function rawstats {
cat /proc/diskstats | egrep "sd\w |mmcblk[0-9] |nvme[0-9]n[0-9] " | ${AWK} "{now=$(date +%s%N);tsr+=\$6; tsw+=\$10; ${PRAW}} END {${PRAWTOT}}" | sort -f
}
# Print "zero stats" when starting from system boot time
function zerostats {
cat /proc/diskstats | egrep "sd\w |mmcblk[0-9] |nvme[0-9]n[0-9] " | ${AWK} "{now=0; \$6=0; \$10=0; ${PRAW}} END {tsr=0; tsw=0; ${PRAWTOT}}" | sort -f
}
# Print device, sectors read, sectors written for devices and total relative to start values
function relstats {
RAW="$(rawstats)"
PASTESTATS=("${RAW}" "${SVALS}")
REL="$(pastestats)"
echo "${REL}" | ${AWK} "{${PREL}}"
}
# Combine newest and oldest stats
# Output: device, FIRST sectors read, FIRST sectors written, FIRST time in seconds, device, SECOND sectors read, SECOND sectors written, SECOND time in seconds
function pastestats {
paste <(echo -n "${PASTESTATS[0]}") <(echo "${PASTESTATS[1]}")
}
# Combine newest and oldest stats
# Output: device, NEWEST sectors read, NEWEST sectors written, NEWEST time in seconds, device, OLDEST sectors read, OLDEST sectors written, OLDEST time in seconds
function combinestats {
# Shift BUF and add new stats, newest data is last
NEW="$(relstats)"
BUF=("${BUF[@]:1}" "${NEW}")
# Combine newest and oldest
PASTESTATS=("${BUF[SC]}" "${BUF[0]}")
ALL="$(pastestats)"
}
function calcstats {
# Headers + erase to end of line
echo -e "${NBSP} Read Written Read Written Xferred Reading Writing Xferring\033[0K"
echo -e "${NBSP} sectors sectors GiBs GiBs GiBs MiB/s MiB/s MiB/s\033[0K"
# awk Cumulative xref speed print
PREL="printf(\"Cumul.rate ${NBSP} ${NBSP} ${NBSP} ${NBSP} ${NBSP} %.2f %.2f %.2f\033[0K\n\", \$2 * ${S2M} / el, \$3 * ${S2M} / el, cx / el)"
NOW="$(date +%s%N)"
${AWK} "{dt=(\$4 - \$8) / ${NS};cx=(\$2 + \$3) * ${S2M}; ${PCALC}} END {el=(${NOW} - ${START}) / ${NS};${PREL}}"
}
# Read cursor position to CPOS array where 0=row and 1=col
# See: https://stackoverflow.com/a/51930257
function readcpos {
local v=() t=$(stty -g)
stty -echo
printf "\033[6n"
IFS='[;' read -ra v -d R
stty $t
CPOS=("${v[@]:1}")
}
# Update relative time. Bash won't do floating point so awk ftw.
AWKRELTIME="{
dt = (\$2 - \$1) / ${NS};
d = int(dt / 60 / 60 / 24);
h = int(dt / 60 / 60 % 24);
m = int(dt / 60 % 60);
s = int(dt % 60);
if (d > 0) sd = sprintf(\"%d day%s \", d, d != 1 ? \"s\" : \"\");
if (h > 0) sh = sprintf(\"%d hour%s \", h, h != 1 ? \"s\" : \"\");
if (m > 0) sm = sprintf(\"%d minute%s \", m, m != 1 ? \"s\" : \"\");
if (s > 0 || d + h + m == 0) ss = sprintf(\"%d second%s \", s, s != 1 ? \"s\" : \"\");
print sd sh sm ss;
}"
function updatereltime {
NOW="$(date +%s%N)"
RELTIME="$(${AWK} "${AWKRELTIME}" <<< "${START} ${NOW}")"
}
# Update time string printed at top
function updatetime {
case "${TF}" in
RUN)
updatereltime
TIMESTR="Stats for last ${RELTIME}"
;;
FULL)
updatereltime
TIMESTR="From ${STARTDATE} to $(date +%c) for ${RELTIME}"
;;
FROMSTART)
updatereltime
TIMESTR="From ${STARTDATE} for ${RELTIME}"
shift
;;
*)
TIMESTR="From ${STARTDATE} to $(date +%c)"
;;
esac
}
# Do the dump
function dumpstats {
# Date to top left corner + erase to end of line
#echo -e "\033[0;0HFrom ${STARTDATE} to $(date)\033[0K"
updatetime
echo -e "\033[0;0H${TIMESTR}\033[0K"
# Dump stats
echo "${ALL}"| calcstats | column -t -s " "
# Erase to end of screen.
# Just to clear any accidental key presses and such.
echo -en "\033[0J"
}
RUNNING=true
# Trap SIGINT to stop
trap interrupted INT
function interrupted {
RUNNING=false
}
# Store start time and date and init start values
if [ "${FROMBOOT}" = true ]; then
BOOTTIME="$(cut -f1 -d' ' /proc/uptime) seconds ago"
START="$(date -d "${BOOTTIME}" +'%s%N')"
STARTDATE="$(date -d "${BOOTTIME}" +%c)"
SVALS="$(zerostats)"
else
START="$(date +%s%N)"
STARTDATE="$(date +%c)"
SVALS="$(rawstats)"
fi
# Init BUF
for i in $(seq 1 $((SC + 1))); do
BUF=("${BUF[@]}" "$(relstats)")
done
# Clear screen by scrolling old stuff out. This way last dump
# gets printed nicely after any previous commands.
# printf is tricked to repeat n times with seq.
readcpos
printf "\033[1S%.0s" $(seq 2 ${CPOS[0]})
# Hide cursor, use alternate buffer and erase the whole screen
echo -en "\033[?25l\033[?1049h\033[0;0H\033[0J"
# Loop until stopped.
while true; do
# Put newest and oldest stats together to ALL
combinestats
# Dump stats
dumpstats
# Exit loop at the end to keep last dump nice and clean.
sleep ${INT}
if ! [ "${RUNNING}" = true ]; then break; fi
done
# Back to original buffer and dump stats there too so
# they'll show up nicely in terminal scrollback.
echo -e "\033[?1049l"
dumpstats
# Reset any possible ansi styles and make cursor visible again.
echo -e "\033[0m\033[?25h"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment