Last active
April 19, 2020 13:57
-
-
Save nuumio/574ac198a7f33db9e459106d0499c8da to your computer and use it in GitHub Desktop.
Disk stat watcher
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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