Created
April 15, 2025 23:45
-
-
Save amedee/3e52957892706385866a41e3256271fa to your computer and use it in GitHub Desktop.
Benchmarking USB Drives
This file contains hidden or 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 | |
# ========================== | |
# CONFIGURATION | |
# ========================== | |
RESULTS=() | |
USB_DEVICE="" | |
TEST_FILE="" | |
RUNS=1 | |
VISUAL="none" | |
SUMMARY=0 | |
# (Consider grouping related configuration into a config file or associative array if script expands) | |
# ========================== | |
# ARGUMENT PARSING | |
# ========================== | |
while [[ $# -gt 0 ]]; do | |
case $1 in | |
--device) | |
USB_DEVICE="$2" | |
shift 2 | |
;; | |
--file) | |
TEST_FILE="$2" | |
shift 2 | |
;; | |
--runs) | |
RUNS="$2" | |
shift 2 | |
;; | |
--visual) | |
VISUAL="$2" | |
shift 2 | |
;; | |
--summary) | |
SUMMARY=1 | |
shift | |
;; | |
--yes|--force) | |
FORCE_YES=1 | |
shift | |
;; | |
*) | |
echo "Unknown option: $1" | |
exit 1 | |
;; | |
esac | |
done | |
# ========================== | |
# TOOL CHECK | |
# ========================== | |
ALL_TOOLS=(hdparm dd pv ioping fio lsblk stat grep awk find sort basename column gnuplot) | |
MISSING_TOOLS=() | |
require() { | |
if ! command -v "$1" >/dev/null; then | |
return 1 | |
fi | |
return 0 | |
} | |
check_required_tools() { | |
echo "π Checking required tools..." | |
for tool in "${ALL_TOOLS[@]}"; do | |
if ! require "$tool"; then | |
MISSING_TOOLS+=("$tool") | |
fi | |
done | |
if [[ ${#MISSING_TOOLS[@]} -gt 0 ]]; then | |
echo "β οΈ The following tools are missing: ${MISSING_TOOLS[*]}" | |
echo "You can install them using: sudo apt install ${MISSING_TOOLS[*]}" | |
if [[ -z "$FORCE_YES" ]]; then | |
read -rp "Do you want to continue and skip tests that require them? (y/N): " yn | |
case $yn in | |
[Yy]*) | |
echo "Continuing with limited tests..." | |
;; | |
*) | |
echo "Aborting. Please install the required tools." | |
exit 1 | |
;; | |
esac | |
else | |
echo "Continuing with limited tests (auto-confirmed)..." | |
fi | |
else | |
echo "β All required tools are available." | |
fi | |
} | |
# ========================== | |
# AUTO-DETECT USB DEVICE | |
# ========================== | |
detect_usb() { | |
if [[ -n "$USB_DEVICE" ]]; then | |
echo "π Using provided USB device: $USB_DEVICE" | |
MOUNT_PATH=$(lsblk -no MOUNTPOINT "$USB_DEVICE") | |
return | |
fi | |
echo "π Detecting USB device..." | |
USB_DEVICE="" | |
while read -r dev tran hotplug type _; do | |
if [[ "$tran" == "usb" && "$hotplug" == "1" && "$type" == "disk" ]]; then | |
base="/dev/$dev" | |
part=$(lsblk -nr -o NAME,MOUNTPOINT "$base" | awk '$2 != "" {print "/dev/"$1; exit}') | |
if [[ -n "$part" ]]; then | |
USB_DEVICE="$part" | |
break | |
fi | |
fi | |
done < <(lsblk -o NAME,TRAN,HOTPLUG,TYPE,MOUNTPOINT -nr) | |
if [ -z "$USB_DEVICE" ]; then | |
echo "β No mounted USB partition found on any USB disk." | |
lsblk -o NAME,TRAN,HOTPLUG,TYPE,SIZE,MOUNTPOINT -nr | grep part | |
read -rp "Enter the USB device path manually (e.g., /dev/sdc1): " USB_DEVICE | |
fi | |
MOUNT_PATH=$(lsblk -no MOUNTPOINT "$USB_DEVICE") | |
if [ -z "$MOUNT_PATH" ]; then | |
echo "β USB device is not mounted." | |
exit 1 | |
fi | |
echo "β Using USB device: $USB_DEVICE" | |
echo "β Mounted at: $MOUNT_PATH" | |
} | |
# ========================== | |
# FIND TEST FILE | |
# ========================== | |
find_ubuntu_iso() { | |
# Function to find an Ubuntu ISO on the USB device | |
find "$MOUNT_PATH" -type f -regextype posix-extended \ | |
-regex ".*/ubuntu-[0-9]{2}\.[0-9]{2}-desktop-amd64\\.iso" | sort -V | tail -n1 | |
} | |
find_test_file() { | |
if [[ -n "$TEST_FILE" ]]; then | |
echo "π Using provided test file: $(basename "$TEST_FILE")" | |
# Check if the provided test file is on the USB device | |
TEST_FILE_MOUNT_PATH=$(realpath "$TEST_FILE" | grep -oP "^$MOUNT_PATH") | |
if [[ -z "$TEST_FILE_MOUNT_PATH" ]]; then | |
echo "β The provided test file is not located on the USB device." | |
# Look for an Ubuntu ISO if it's not on the USB | |
TEST_FILE=$(find_ubuntu_iso) | |
fi | |
else | |
TEST_FILE=$(find_ubuntu_iso) | |
fi | |
if [ -z "$TEST_FILE" ]; then | |
echo "β No valid test file found." | |
exit 1 | |
fi | |
if [[ "$TEST_FILE" =~ ubuntu-[0-9]{2}\.[0-9]{2}-desktop-amd64\.iso ]]; then | |
UBUNTU_VERSION=$(basename "$TEST_FILE" | grep -oP 'ubuntu-\d{2}\.\d{2}') | |
echo "π§ͺ Selected Ubuntu version: $UBUNTU_VERSION" | |
else | |
echo "π Selected test file: $(basename "$TEST_FILE")" | |
fi | |
} | |
# ========================== | |
# SPEED EXTRACTION | |
# ========================== | |
extract_speed() { | |
grep -oP '(?i)[\d.,]+\s*[KMG]i?B/s' | tail -1 | sed 's/,/./' | |
} | |
speed_to_mb() { | |
if [[ "$1" =~ ([0-9.,]+)[[:space:]]*([a-zA-Z/]+) ]]; then | |
value="${BASH_REMATCH[1]}" | |
unit=$(echo "${BASH_REMATCH[2]}" | tr '[:upper:]' '[:lower:]') | |
else | |
echo "0" | |
return | |
fi | |
case "$unit" in | |
kb/s) awk -v v="$value" 'BEGIN { printf "%.2f", v / 1000 }' ;; | |
mb/s) awk -v v="$value" 'BEGIN { printf "%.2f", v }' ;; | |
gb/s) awk -v v="$value" 'BEGIN { printf "%.2f", v * 1000 }' ;; | |
kib/s) awk -v v="$value" 'BEGIN { printf "%.2f", v / 1024 }' ;; | |
mib/s) awk -v v="$value" 'BEGIN { printf "%.2f", v }' ;; | |
gib/s) awk -v v="$value" 'BEGIN { printf "%.2f", v * 1024 }' ;; | |
*) echo "0" ;; | |
esac | |
} | |
drop_caches() { | |
echo "π§Ή Dropping system caches..." | |
if [[ $EUID -ne 0 ]]; then | |
echo " (requires sudo)" | |
fi | |
sudo sh -c "sync && echo 3 > /proc/sys/vm/drop_caches" | |
} | |
# ========================== | |
# RUN BENCHMARKS | |
# ========================== | |
run_benchmarks() { | |
echo "π Read-only USB benchmark started ($RUNS run(s))" | |
echo "===================================" | |
declare -A TEST_NAMES=( | |
[1]="hdparm" | |
[2]="dd" | |
[3]="dd + pv" | |
[4]="cat + pv" | |
[5]="ioping" | |
[6]="fio" | |
) | |
declare -A TOTAL_MB | |
for i in {1..6}; do TOTAL_MB[$i]=0; done | |
CSVFILE="usb-benchmark-$(date +%Y%m%d-%H%M%S).csv" | |
echo "Test,Run,Speed (MB/s)" > "$CSVFILE" | |
for ((run=1; run<=RUNS; run++)); do | |
echo "βΆ Run $run" | |
idx=1 | |
if require hdparm; then | |
drop_caches | |
speed=$(sudo hdparm -t --direct "$USB_DEVICE" 2>/dev/null | extract_speed) | |
mb=$(speed_to_mb "$speed") | |
echo "${idx}. ${TEST_NAMES[$idx]}: $speed" | |
TOTAL_MB[$idx]=$(echo "${TOTAL_MB[$idx]} + $mb" | bc) | |
echo "${TEST_NAMES[$idx]},$run,$mb" >> "$CSVFILE" | |
fi | |
((idx++)) | |
drop_caches | |
speed=$(dd if="$TEST_FILE" of=/dev/null bs=8k 2>&1 |& extract_speed) | |
mb=$(speed_to_mb "$speed") | |
echo "${idx}. ${TEST_NAMES[$idx]}: $speed" | |
TOTAL_MB[$idx]=$(echo "${TOTAL_MB[$idx]} + $mb" | bc) | |
echo "${TEST_NAMES[$idx]},$run,$mb" >> "$CSVFILE" | |
((idx++)) | |
if require pv; then | |
drop_caches | |
FILESIZE=$(stat -c%s "$TEST_FILE") | |
speed=$(dd if="$TEST_FILE" bs=8k status=none | pv -s "$FILESIZE" -f -X 2>&1 | extract_speed) | |
mb=$(speed_to_mb "$speed") | |
echo "${idx}. ${TEST_NAMES[$idx]}: $speed" | |
TOTAL_MB[$idx]=$(echo "${TOTAL_MB[$idx]} + $mb" | bc) | |
echo "${TEST_NAMES[$idx]},$run,$mb" >> "$CSVFILE" | |
fi | |
((idx++)) | |
if require pv; then | |
drop_caches | |
speed=$(cat "$TEST_FILE" | pv -f -X 2>&1 | extract_speed) | |
mb=$(speed_to_mb "$speed") | |
echo "${idx}. ${TEST_NAMES[$idx]}: $speed" | |
TOTAL_MB[$idx]=$(echo "${TOTAL_MB[$idx]} + $mb" | bc) | |
echo "${TEST_NAMES[$idx]},$run,$mb" >> "$CSVFILE" | |
fi | |
((idx++)) | |
if require ioping; then | |
drop_caches | |
speed=$(ioping -c 10 -A "$USB_DEVICE" 2>/dev/null | grep 'read' | extract_speed) | |
mb=$(speed_to_mb "$speed") | |
echo "${idx}. ${TEST_NAMES[$idx]}: $speed" | |
TOTAL_MB[$idx]=$(echo "${TOTAL_MB[$idx]} + $mb" | bc) | |
echo "${TEST_NAMES[$idx]},$run,$mb" >> "$CSVFILE" | |
fi | |
((idx++)) | |
if require fio; then | |
drop_caches | |
speed=$(fio --name=readtest --filename="$TEST_FILE" --direct=1 --rw=read --bs=8k \ | |
--size=100M --ioengine=libaio --iodepth=16 --runtime=5s --time_based --readonly \ | |
--minimal 2>/dev/null | awk -F';' '{print $6" KB/s"}' | extract_speed) | |
mb=$(speed_to_mb "$speed") | |
echo "${idx}. ${TEST_NAMES[$idx]}: $speed" | |
TOTAL_MB[$idx]=$(echo "${TOTAL_MB[$idx]} + $mb" | bc) | |
echo "${TEST_NAMES[$idx]},$run,$mb" >> "$CSVFILE" | |
fi | |
done | |
echo "π Summary of average results for $UBUNTU_VERSION:" | |
echo "===================================" | |
SUMMARY_TABLE="" | |
for i in {1..6}; do | |
if [[ ${TOTAL_MB[$i]} != 0 ]]; then | |
avg=$(echo "scale=2; ${TOTAL_MB[$i]} / $RUNS" | bc) | |
echo "${TEST_NAMES[$i]} average: $avg MB/s" | |
RESULTS+=("${TEST_NAMES[$i]} average: $avg MB/s") | |
SUMMARY_TABLE+="${TEST_NAMES[$i]},$avg\n" | |
fi | |
done | |
if [[ "$VISUAL" == "table" || "$VISUAL" == "both" ]]; then | |
echo -e "π Table view:" | |
echo -e "Test Method,Average MB/s\n$SUMMARY_TABLE" | column -t -s ',' | |
fi | |
if [[ "$VISUAL" == "bar" || "$VISUAL" == "both" ]]; then | |
if require gnuplot; then | |
echo -e "$SUMMARY_TABLE" | awk -F',' '{print $1" "$2}' | \ | |
gnuplot -p -e " | |
set terminal dumb; | |
set title 'USB Read Benchmark Results ($UBUNTU_VERSION)'; | |
set xlabel 'Test Method'; | |
set ylabel 'MB/s'; | |
plot '-' using 2:xtic(1) with boxes notitle | |
" | |
fi | |
fi | |
LOGFILE="usb-benchmark-$(date +%Y%m%d-%H%M%S).log" | |
{ | |
echo "Benchmark for USB device: $USB_DEVICE" | |
echo "Mounted at: $MOUNT_PATH" | |
echo "Ubuntu version: $UBUNTU_VERSION" | |
echo "Test file: $TEST_FILE" | |
echo "Timestamp: $(date)" | |
echo "Number of runs: $RUNS" | |
echo "" | |
echo "Read speed averages:" | |
for line in "${RESULTS[@]}"; do | |
echo "$line" | |
done | |
} > "$LOGFILE" | |
echo "π Results saved to: $LOGFILE" | |
echo "π CSV exported to: $CSVFILE" | |
echo "===================================" | |
} | |
# ========================== | |
# MAIN | |
# ========================== | |
check_required_tools | |
detect_usb | |
find_test_file | |
run_benchmarks |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment