New python speedtest to replace bash. New CLAUDE.md
This commit is contained in:
+307
-302
@@ -1,8 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Robust HD/SDD/NVMe performance CLI utility
|
||||
# Utilizing FIO for sequential/random writes/writes
|
||||
# Dependencies: fio (apt install fio)
|
||||
# Robust HD/SSD/NVMe performance CLI utility
|
||||
# A CrystalDiskMark-style storage benchmark for Linux, built on fio.
|
||||
# Runs the exact same four tests CrystalDiskMark does (SEQ1M Q8T1, SEQ1M Q1T1,
|
||||
# RND4K Q32T1, RND4K Q32T16), each measured for both Read and Write, and prints
|
||||
# the results in a CrystalDiskMark-style table.
|
||||
# Dependencies: fio (apt install fio / pacman -S fio). Falls back to dd if missing.
|
||||
# See: https://cloud.google.com/compute/docs/disks/benchmarking-pd-performance
|
||||
# See: https://arstechnica.com/gadgets/2020/02/how-fast-are-your-disks-find-out-the-open-source-way-with-fio/
|
||||
# mReschke 2024-01-18
|
||||
@@ -10,20 +13,23 @@
|
||||
# CLI Parameters
|
||||
path="$1"
|
||||
option=""
|
||||
simple=false
|
||||
engine="" # IO engine; empty = auto-detect (io_uring > libaio > posixaio > sync)
|
||||
direct="" # O_DIRECT; empty = auto-detect; 1 = bypass page cache, 0 = buffered
|
||||
runtime=30 # seconds per test (longer = more accurate, exposes SLC cache exhaustion)
|
||||
runtime=5 # seconds per measurement (CrystalDiskMark default is 5s)
|
||||
size="1g" # test file size (CrystalDiskMark default is 1GiB)
|
||||
verbose=0 # 1 = also print the full fio output for every run
|
||||
for arg in "${@:2}"; do
|
||||
case "$arg" in
|
||||
--simple) simple=true ;;
|
||||
--dd) option="--dd" ;;
|
||||
--fio) option="--fio" ;;
|
||||
--slog) option="--slog" ;;
|
||||
--buffered) direct=0 ;;
|
||||
--direct) direct=1 ;;
|
||||
--verbose) verbose=1 ;;
|
||||
--engine=*) engine="${arg#*=}" ;;
|
||||
--direct=*) direct="${arg#*=}" ;;
|
||||
--runtime=*) runtime="${arg#*=}" ;;
|
||||
--size=*) size="${arg#*=}" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -47,275 +53,35 @@ function main {
|
||||
fi
|
||||
|
||||
# Must type y or n THEN press enter (which I like better)
|
||||
echo "NOTICE: 1GB free space on '$path' is required to perform the benchmark."
|
||||
echo -n "Are you ready to start a robust IO benchmark against '$path' ?"; read answer
|
||||
echo "NOTICE: ${size^^} free space on '$path' is required to perform the benchmark."
|
||||
echo -n "Are you ready to start a storage benchmark against '$path' ? "; read answer
|
||||
if [ "$answer" != "${answer#[Yy]}" ]; then
|
||||
echo "Great! Starting benchmark now!"
|
||||
echo "------------------------------"
|
||||
else
|
||||
echo "Ok, cancelled!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Use dd of fio based on param or defaults
|
||||
# Use dd or fio based on param or defaults
|
||||
if [ "$option" == "--dd" ]; then
|
||||
dd_speedtest
|
||||
elif [ "$option" == "--fio" ]; then
|
||||
fio_speedtest
|
||||
cdm_speedtest
|
||||
elif [ "$option" == "--slog" ]; then
|
||||
slog_speedtest
|
||||
elif [ "$option" == "" ]; then
|
||||
# If fio is installed, use it, else use dd
|
||||
[ "$simple" != true ] && echo ""
|
||||
if ! command -v fio &> /dev/null; then
|
||||
echo ""
|
||||
echo "fio is not installed -- falling back to basic dd test."
|
||||
echo "Install fio for the full CrystalDiskMark-style benchmark."
|
||||
dd_speedtest
|
||||
else
|
||||
fio_speedtest
|
||||
cdm_speedtest
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function print_result {
|
||||
# Print a single fio test result, either as the full "Run status group"
|
||||
# line or, when --simple is given, as a compact aligned "Label value" row.
|
||||
# The simple value is taken from the parenthetical (MB/s) figure that
|
||||
# follows the first bw= value in fio's output.
|
||||
local label="$1"
|
||||
local output="$2"
|
||||
local status
|
||||
status=$(echo " - $output " | /usr/bin/grep -A1 'Run status group' | tail -n1)
|
||||
if [ "$simple" == true ]; then
|
||||
local value
|
||||
# Grab the parenthetical (MB/s) figure after the first bw=, then put a
|
||||
# space between the number and the unit, eg "98.4MB/s" -> "98.4 MB/s".
|
||||
value=$(echo "$status" | /usr/bin/grep -oP 'bw=\S+\s+\(\K[^)]+' | sed -E 's/([0-9.]+)([A-Za-z])/\1 \2/')
|
||||
printf "%-28s%s\n" "$label" "$value"
|
||||
else
|
||||
echo "$status"
|
||||
fi
|
||||
}
|
||||
|
||||
function fio_write_single_random_4k {
|
||||
# 4K Random Writes (parallel, deep queue)
|
||||
|
||||
# Random 4K writes are the worst thing you can ask a disk to do. Where this
|
||||
# happens most in real life: copying home directories and dotfiles, email
|
||||
# stores, some database operations, source code trees.
|
||||
|
||||
# IMPORTANT: the original version of this test used numjobs=1 + iodepth=1.
|
||||
# That measures single-op LATENCY (the absolute worst case): the OS issues
|
||||
# one 4K write, waits for it to be acknowledged, then issues the next. On
|
||||
# NVMe that lands around ~3000 IOPS / ~12MB/sec -- which looks alarmingly
|
||||
# slow but is NOT a bug. NVMe's entire advantage is deep command queues and
|
||||
# parallelism, so a queue depth of 1 deliberately measures the one scenario
|
||||
# NVMe is bad at. To measure the drive's actual capability we now drive it
|
||||
# with multiple jobs and a deep queue (4 jobs x iodepth=64), keeping
|
||||
# hundreds of operations in flight at once. Expect this to jump into the
|
||||
# hundreds of MB/sec (often 1GB/sec+) on a healthy NVMe pool.
|
||||
|
||||
# --name= is a required argument, but it's basically human-friendly fluff—fio will create files based on that name to test with, inside the directory you point it at.
|
||||
# --directory= where the test files are created; this is the path/mount you are benchmarking.
|
||||
# --ioengine=$engine the engine fio uses to talk to the kernel, auto-detected by detect_io_settings(). On modern Linux this is io_uring (lowest overhead); it falls back to libaio, then posixaio, then sync. NOTE: libaio is only truly asynchronous with --direct=1; with buffered IO it silently degrades to synchronous, which is exactly why io_uring is preferred.
|
||||
# --rw=randwrite random write operations. Other options include read, write (sequential), randread, and randrw.
|
||||
# --bs=4k blocksize 4K. These are very small individual operations. This is where the pain lives; it's hard on the disk and adds a ton of command-channel overhead, since a separate operation has to be commanded for each 4K of data.
|
||||
# --size=256m each job's test file is 256MB. With --numjobs=4 that's 4 files totaling ~1GB.
|
||||
# --numjobs=4 run 4 parallel processes, each with its own test file, to feed the drive from multiple threads at once (single-threaded cannot saturate NVMe).
|
||||
# --iodepth=64 how many commands each job keeps stacked in the queue at once. Deep queues are how NVMe reaches its real IOPS; 4 jobs x 64 = up to 256 ops in flight.
|
||||
# --direct=$direct O_DIRECT, auto-detected. 1 bypasses the OS page cache so we measure the device instead of RAM; 0 (buffered) is the fallback for filesystems/mounts that reject O_DIRECT (eg older OpenZFS, some NFS).
|
||||
# --ramp_time=2s discard the first 2 seconds of results so we measure steady state, not the initial warm-up burst.
|
||||
# --runtime=$runtime --time_based run for this many seconds, looping over the file(s) if we finish early.
|
||||
# --group_reporting=1 aggregate all jobs into a single combined result line instead of one line per job.
|
||||
# --end_fsync=1 after the timed run, flush everything to stable storage and count that time, so cached writes can't inflate the number.
|
||||
[ "$simple" != true ] && { echo ""; echo "4K Random Writes (bs=4k, jobs=4, iodepth=64, engine=$engine, direct=$direct, ${runtime}s)"; }
|
||||
x=`sudo fio \
|
||||
--name=fio-write-random-4k \
|
||||
--directory=$path \
|
||||
--ioengine=$engine \
|
||||
--rw=randwrite \
|
||||
--bs=4k \
|
||||
--size=256m \
|
||||
--numjobs=4 \
|
||||
--iodepth=64 \
|
||||
--direct=$direct \
|
||||
--ramp_time=2s \
|
||||
--time_based --runtime=$runtime \
|
||||
--group_reporting=1 \
|
||||
--end_fsync=1`
|
||||
print_result "4K Random Writes" "$x"
|
||||
|
||||
# Cleanup my test files
|
||||
rm -rf $path/fio-write-random-4k*
|
||||
}
|
||||
|
||||
function fio_write_parallel_random_64k {
|
||||
# Parallel 64k Random Writes
|
||||
|
||||
# This time, we're creating 16 separate 64MB files (still totaling 1GB, when
|
||||
# all put together) and we're issuing 64KB blocksized random write operations.
|
||||
# We're doing it with sixteen separate processes running in parallel, and
|
||||
# we're queuing up to 16 simultaneous asynchronous ops before we pause and wait
|
||||
# for the OS to start acknowledging their receipt.
|
||||
|
||||
# This is a pretty decent approximation of a significantly busy system. It's
|
||||
# not doing any one particularly nasty thing—like running a database engine or
|
||||
# copying tons of dotfiles from a user's home directory—but it is coping with
|
||||
# a bunch of applications doing moderately demanding stuff all at once.
|
||||
|
||||
# This is also a pretty good, slightly pessimistic approximation of a busy,
|
||||
# multi-user system like a NAS, which needs to handle multiple 1MB operations
|
||||
# simultaneously for different users. If several people or processes are trying
|
||||
# to read or write big files (photos, movies, whatever) at once, the OS tries
|
||||
# to feed them all data simultaneously. This pretty quickly devolves down to a
|
||||
# pattern of multiple random small block access. So in addition to "busy desktop
|
||||
# with lots of apps," think "busy fileserver with several people actively using it."
|
||||
|
||||
# You will see a lot more variation in speed as you watch this operation play
|
||||
# out on the console. Unlike the steady trickle you'd get from a queue-depth-1
|
||||
# single-op latency test, this 16-process job can fluctuate wildly—eg between
|
||||
# about 10MiB/sec and 300MiB/sec during the run—as the OS and SSD firmware
|
||||
# catch good and bad luck aggregating writes.
|
||||
|
||||
# Most of the variation you're seeing here is due to the operating system and
|
||||
# SSD firmware sometimes being able to aggregate multiple writes. When it
|
||||
# manages to aggregate them helpfully, it can write them in a way that allows
|
||||
# parallel writes to all the individual physical media stripes inside the SSD.
|
||||
# Sometimes, it still ends up having to give up and write to only a single
|
||||
# physical media stripe at a time—or a garbage collection or other maintenance
|
||||
# operation at the SSD firmware level needs to run briefly in the background,
|
||||
# slowing things down.
|
||||
[ "$simple" != true ] && { echo ""; echo "Parallel 64K Random Writes (bs=64k, jobs=16, iodepth=16, engine=$engine, direct=$direct, ${runtime}s)"; }
|
||||
x=`sudo fio \
|
||||
--name=fio-write-random-64k \
|
||||
--directory=$path \
|
||||
--ioengine=$engine \
|
||||
--rw=randwrite \
|
||||
--bs=64k \
|
||||
--size=64m \
|
||||
--numjobs=16 \
|
||||
--iodepth=16 \
|
||||
--direct=$direct \
|
||||
--ramp_time=2s \
|
||||
--time_based --runtime=$runtime \
|
||||
--group_reporting=1 \
|
||||
--end_fsync=1`
|
||||
print_result "Parallel 64K Random Writes" "$x"
|
||||
|
||||
# Cleanup my test files
|
||||
rm -rf $path/fio-write-random-64k*
|
||||
}
|
||||
|
||||
function fio_write_single_sequential_1m {
|
||||
# Single 1M Sequential Writes
|
||||
|
||||
# This is pretty close to the best-case scenario for a real-world system
|
||||
# doing real-world things. No, it's not quite as fast as a single, truly
|
||||
# contiguous write... but the 1MiB blocksize is large enough that it's quite
|
||||
# close. Besides, if literally any other disk activity is requested simultaneously
|
||||
# with a contiguous write, the "contiguous" write devolves to this level of
|
||||
# performance pretty much instantly, so this is a much more realistic test of
|
||||
# the upper end of storage performance on a typical system.
|
||||
|
||||
# You'll see some kooky fluctuations on SSDs when doing this test. This is largely
|
||||
# due to the SSD's firmware having better luck or worse luck at any given time,
|
||||
# when it's trying to queue operations so that it can write across all physical
|
||||
# media stripes cleanly at once. Rust disks will tend to provide a much more
|
||||
# consistent, though typically lower, throughput across the run.
|
||||
|
||||
# You can also see SSD performance fall off a cliff here if you exhaust an
|
||||
# onboard write cache—TLC and QLC drives tend to have small write cache areas
|
||||
# made of much faster MLC or SLC media. Once those get exhausted, the disk has
|
||||
# to drop to writing directly to the much slower TLC/QLC media where the data
|
||||
# eventually lands. This is the major difference between, for example, Samsung
|
||||
# EVO and Pro SSDs—the EVOs have slow TLC media with a fast MLC cache, where
|
||||
# the Pros use the higher-performance, higher-longevity MLC media throughout
|
||||
# the entire SSD.
|
||||
|
||||
# If you have any doubt at all about a TLC or QLC disk's ability to sustain
|
||||
# heavy writes, you may want to experimentally extend your time duration here.
|
||||
# If you watch the throughput live as the job progresses, you'll see the impact
|
||||
# immediately when you run out of cache—what had been a fairly steady,
|
||||
# several-hundred-MiB/sec throughput will suddenly plummet to half the speed
|
||||
# or less and get considerably less stable as well.
|
||||
|
||||
# However, you might choose to take the opposite position—you might not
|
||||
# expect to do sustained heavy writes very frequently, in which case you
|
||||
# actually are more interested in the on-cache behavior. What's important
|
||||
# here is that you understand both what you want to test, and how to test
|
||||
# it accurately.
|
||||
|
||||
[ "$simple" != true ] && { echo ""; echo "Single 1M Sequential Writes (bs=1m, jobs=1, iodepth=16, engine=$engine, direct=$direct, ${runtime}s)"; }
|
||||
x=`sudo fio \
|
||||
--name=fio-write-seq-1m \
|
||||
--directory=$path \
|
||||
--ioengine=$engine \
|
||||
--rw=write \
|
||||
--bs=1m \
|
||||
--size=1g \
|
||||
--numjobs=1 \
|
||||
--iodepth=16 \
|
||||
--direct=$direct \
|
||||
--ramp_time=2s \
|
||||
--time_based --runtime=$runtime \
|
||||
--group_reporting=1 \
|
||||
--end_fsync=1`
|
||||
print_result "Single 1M Sequential Writes" "$x"
|
||||
|
||||
# Cleanup my test files
|
||||
rm -rf $path/fio-write-seq-1m*
|
||||
}
|
||||
|
||||
function fio_read_sequential_1m {
|
||||
# Sequential Parallel Reads
|
||||
|
||||
[ "$simple" != true ] && { echo ""; echo "Sequential 4x 1M Reads (bs=1m, jobs=4, iodepth=64, engine=$engine, direct=$direct, ${runtime}s)"; }
|
||||
x=`sudo fio \
|
||||
--name=fio-read-sequential-1m \
|
||||
--directory=$path \
|
||||
--ioengine=$engine \
|
||||
--bs=1M \
|
||||
--numjobs=4 \
|
||||
--size=256M \
|
||||
--time_based --runtime=$runtime \
|
||||
--ramp_time=2s \
|
||||
--direct=$direct \
|
||||
--verify=0 \
|
||||
--iodepth=64 \
|
||||
--rw=read \
|
||||
--group_reporting=1 \
|
||||
--iodepth_batch_submit=64 \
|
||||
--iodepth_batch_complete_max=64`
|
||||
print_result "Sequential 4x 1M Reads" "$x"
|
||||
rm -rf $path/fio-read-sequential-1m*
|
||||
}
|
||||
|
||||
function fio_read_random_4k {
|
||||
# Random 4k Reads (parallel, deep queue)
|
||||
|
||||
# 4K random reads are the highest-IOPS thing a drive does, but a single
|
||||
# submitting thread goes CPU-bound long before an NVMe runs out of capacity,
|
||||
# so it under-reports peak IOPS. We spread the work over 4 jobs x iodepth=64
|
||||
# (256 ops in flight, like the 4K write test) to saturate fast drives, while
|
||||
# still reporting the honest (low) number on a spinning HDD. size=256m per
|
||||
# job keeps the total footprint at ~1GB across the 4 jobs.
|
||||
[ "$simple" != true ] && { echo ""; echo "Random 4k Reads (bs=4k, jobs=4, iodepth=64, engine=$engine, direct=$direct, ${runtime}s)"; }
|
||||
x=`sudo fio \
|
||||
--name=fio-read-random-4k \
|
||||
--directory=$path \
|
||||
--ioengine=$engine \
|
||||
--rw=randread \
|
||||
--bs=4k \
|
||||
--size=256m \
|
||||
--numjobs=4 \
|
||||
--time_based --runtime=$runtime \
|
||||
--ramp_time=2s \
|
||||
--direct=$direct \
|
||||
--verify=0 \
|
||||
--iodepth=64 \
|
||||
--group_reporting=1 \
|
||||
--iodepth_batch_submit=64 \
|
||||
--iodepth_batch_complete_max=64`
|
||||
print_result "Random 4k Reads" "$x"
|
||||
rm -rf $path/fio-read-random-4k*
|
||||
}
|
||||
|
||||
function fio_probe {
|
||||
# Run a tiny throwaway fio job to see if a given (engine, direct) combo
|
||||
# actually works on this path/filesystem. Returns 0 on success. This is how
|
||||
@@ -353,57 +119,291 @@ function detect_io_settings {
|
||||
if [ -z "$direct" ]; then
|
||||
if fio_probe "$engine" 1; then direct=1; else direct=0; fi
|
||||
fi
|
||||
|
||||
if [ "$simple" != true ]; then
|
||||
echo ""
|
||||
echo "IO engine : $engine"
|
||||
if [ "$direct" == 1 ]; then
|
||||
echo "O_DIRECT : enabled (bypassing page cache -- measuring the device)"
|
||||
else
|
||||
echo "O_DIRECT : DISABLED (buffered) -- this filesystem/mount rejected"
|
||||
echo " O_DIRECT, so results (especially reads) may reflect the"
|
||||
echo " RAM cache rather than the underlying device. Pass"
|
||||
echo " --direct to force it if you believe it should work."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function fio_speedtest {
|
||||
function fio_bw_mbps {
|
||||
# Extract bandwidth in decimal MB/s (matching CrystalDiskMark) from a fio
|
||||
# run. fio prints eg " READ: bw=3357MiB/s (3521MB/s), ..." -- we take the
|
||||
# parenthetical SI figure and normalise kB/MB/GB into MB/s.
|
||||
local out="$1"
|
||||
local raw num unit
|
||||
raw=$(echo "$out" | /usr/bin/grep -oP 'bw=\S+\s+\(\K[^)]+' | head -n1)
|
||||
[ -z "$raw" ] && { echo "0.00"; return; }
|
||||
num=$(echo "$raw" | /usr/bin/grep -oP '[0-9.]+' | head -n1)
|
||||
unit=$(echo "$raw" | /usr/bin/grep -oP '[A-Za-z]+/s' | head -n1)
|
||||
case "$unit" in
|
||||
B/s) awk -v n="$num" 'BEGIN{printf "%.2f", n/1000000}' ;;
|
||||
kB/s|KB/s) awk -v n="$num" 'BEGIN{printf "%.2f", n/1000}' ;;
|
||||
MB/s) awk -v n="$num" 'BEGIN{printf "%.2f", n}' ;;
|
||||
GB/s) awk -v n="$num" 'BEGIN{printf "%.2f", n*1000}' ;;
|
||||
*) awk -v n="$num" 'BEGIN{printf "%.2f", n}' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
function fio_iops {
|
||||
# Extract IOPS from a fio run. fio prints eg " read: IOPS=12.3k, BW=..." --
|
||||
# the value may carry a k/M suffix, which we expand to a whole number.
|
||||
local out="$1"
|
||||
local raw num unit
|
||||
raw=$(echo "$out" | /usr/bin/grep -oP 'IOPS=\K[0-9.]+[kKmM]?' | head -n1)
|
||||
[ -z "$raw" ] && { echo "0"; return; }
|
||||
num=$(echo "$raw" | /usr/bin/grep -oP '[0-9.]+' | head -n1)
|
||||
unit=$(echo "$raw" | /usr/bin/grep -oP '[kKmM]' | head -n1)
|
||||
case "$unit" in
|
||||
k|K) awk -v n="$num" 'BEGIN{printf "%.0f", n*1000}' ;;
|
||||
m|M) awk -v n="$num" 'BEGIN{printf "%.0f", n*1000000}' ;;
|
||||
*) awk -v n="$num" 'BEGIN{printf "%.0f", n}' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
function run_fio {
|
||||
# run_fio <rw> <bs> <iodepth> <numjobs> -> echoes "MB/s IOPS" (space-sep)
|
||||
# A single shared 1GB test file is reused across every run (laid out once),
|
||||
# which keeps the footprint at one file and avoids re-creating it each time.
|
||||
local rw="$1" bs="$2" qd="$3" jobs="$4"
|
||||
local args=(
|
||||
--name=speedtest
|
||||
--filename="$benchfile"
|
||||
--ioengine="$engine"
|
||||
--direct="$direct"
|
||||
--rw="$rw"
|
||||
--bs="$bs"
|
||||
--size="$size"
|
||||
--numjobs="$jobs"
|
||||
--iodepth="$qd"
|
||||
--time_based --runtime="$runtime"
|
||||
--group_reporting=1
|
||||
)
|
||||
# Flush device cache at the end of write tests so cached writes can't lie.
|
||||
case "$rw" in
|
||||
*write) args+=(--end_fsync=1) ;;
|
||||
esac
|
||||
# Capture the full fio output. In verbose mode we also keep fio's stderr
|
||||
# (2>&1); otherwise we discard it. Either way only the parsed bandwidth is
|
||||
# echoed to stdout, so the captured value stays clean.
|
||||
local out
|
||||
if [ "$verbose" = 1 ]; then
|
||||
out=$(sudo fio "${args[@]}" 2>&1)
|
||||
else
|
||||
out=$(sudo fio "${args[@]}" 2>/dev/null)
|
||||
fi
|
||||
# Dump the raw fio output (to stderr, so it never pollutes the captured
|
||||
# values on stdout) when running verbose.
|
||||
if [ "$verbose" = 1 ]; then
|
||||
echo " --- fio: $rw bs=$bs iodepth=$qd numjobs=$jobs ---" >&2
|
||||
echo "$out" >&2
|
||||
echo "" >&2
|
||||
fi
|
||||
echo "$(fio_bw_mbps "$out") $(fio_iops "$out")"
|
||||
}
|
||||
|
||||
# Table drawing -------------------------------------------------------------
|
||||
# Label cell is "| %-16s |" (18 dashes); the four numeric cells are "| %14s |"
|
||||
# (16 dashes each).
|
||||
TBL_SEG_LBL="------------------" # 18 dashes -> matches "| %-16s |"
|
||||
TBL_SEG_NUM="----------------" # 16 dashes -> matches "| %14s |"
|
||||
function tbl_rule {
|
||||
printf "+%s+%s+%s+%s+%s+\n" "$TBL_SEG_LBL" "$TBL_SEG_NUM" "$TBL_SEG_NUM" "$TBL_SEG_NUM" "$TBL_SEG_NUM"
|
||||
}
|
||||
function tbl_row {
|
||||
# tbl_row <test> <read-mb/s> <write-mb/s> <read-iops> <write-iops>
|
||||
# (col1 left-aligned, the four numeric cols right-aligned)
|
||||
printf "| %-16s | %14s | %14s | %14s | %14s |\n" "$1" "$2" "$3" "$4" "$5"
|
||||
}
|
||||
|
||||
# Five-column table for the --slog latency profile.
|
||||
# "| %-16s |" -> 18 dashes ; "| %12s |" -> 14 dashes
|
||||
SLOG_SEG16="------------------" # 18 dashes
|
||||
SLOG_SEG12="--------------" # 14 dashes
|
||||
function slog_rule {
|
||||
printf "+%s+%s+%s+%s+%s+\n" "$SLOG_SEG16" "$SLOG_SEG12" "$SLOG_SEG12" "$SLOG_SEG12" "$SLOG_SEG12"
|
||||
}
|
||||
function slog_row {
|
||||
# slog_row <test> <iops> <mb/s> <p50> <p99> (col1 left, rest right-aligned)
|
||||
printf "| %-16s | %12s | %12s | %12s | %12s |\n" "$1" "$2" "$3" "$4" "$5"
|
||||
}
|
||||
|
||||
function run_fio_sync {
|
||||
# run_fio_sync <bs> <numjobs>
|
||||
# -> echoes "IOPS MB/s p50_us p99_us mean_us" (one space-separated line)
|
||||
# Forces synchronous IO (--sync=1 => O_SYNC) with the portable psync engine so
|
||||
# every write is a ZIL commit -- i.e. it exercises the SLOG exactly the way a
|
||||
# sync=always dataset (NFS/iSCSI/VM) does, regardless of the dataset's own
|
||||
# sync property. We deliberately DON'T use --group_reporting here: each job is
|
||||
# reported independently in the JSON and we aggregate ourselves (sum IOPS/bw,
|
||||
# average p50, take the worst p99), which sidesteps fio's group-merge quirks.
|
||||
local bs="$1" jobs="$2"
|
||||
local args=(
|
||||
--name=slog
|
||||
--filename="$benchfile"
|
||||
--ioengine=psync
|
||||
--rw=randwrite
|
||||
--bs="$bs"
|
||||
--size="$size"
|
||||
--numjobs="$jobs"
|
||||
--sync=1
|
||||
--time_based --runtime="$runtime"
|
||||
--output-format=json
|
||||
)
|
||||
local out
|
||||
out=$(sudo fio "${args[@]}" 2>/dev/null)
|
||||
if [ "$verbose" = 1 ]; then
|
||||
echo " --- fio json: randwrite bs=$bs numjobs=$jobs sync=1 ---" >&2
|
||||
echo "$out" >&2
|
||||
echo "" >&2
|
||||
fi
|
||||
# Parse the JSON: sum IOPS and bandwidth across jobs, aggregate completion
|
||||
# latency percentiles (clat is in ns -> convert to us). bw_bytes is bytes/s.
|
||||
echo "$out" | python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
jobs = d["jobs"]
|
||||
except Exception:
|
||||
print("0 0.00 0.0 0.0 0.0"); sys.exit(0)
|
||||
iops = sum(j["write"]["iops"] for j in jobs)
|
||||
mbps = sum(j["write"]["bw_bytes"] for j in jobs) / 1e6
|
||||
def pctl(j, p):
|
||||
return j["write"]["clat_ns"]["percentile"].get(p)
|
||||
p50 = [pctl(j, "50.000000") for j in jobs if pctl(j, "50.000000") is not None]
|
||||
p99 = [pctl(j, "99.000000") for j in jobs if pctl(j, "99.000000") is not None]
|
||||
mean = [j["write"]["clat_ns"]["mean"] for j in jobs]
|
||||
p50_us = (sum(p50)/len(p50))/1000.0 if p50 else 0.0 # average of per-job medians
|
||||
p99_us = (max(p99))/1000.0 if p99 else 0.0 # worst-case tail across jobs
|
||||
mean_us = (sum(mean)/len(mean))/1000.0 if mean else 0.0
|
||||
print("%.0f %.2f %.1f %.1f %.1f" % (iops, mbps, p50_us, p99_us, mean_us))
|
||||
'
|
||||
}
|
||||
|
||||
function slog_speedtest {
|
||||
# SLOG / sync-write latency profile. Detect engine/direct only for the banner;
|
||||
# the actual runs always force psync + O_SYNC (see run_fio_sync).
|
||||
detect_io_settings
|
||||
benchfile="$path/speedtest-hd.bench"
|
||||
|
||||
if ! command -v fio &> /dev/null; then
|
||||
echo "ERROR: --slog requires fio (apt install fio / pacman -S fio)." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "ERROR: --slog requires python3 (used to parse fio JSON latency percentiles)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " speedtest-hd : SLOG / sync-write latency profile"
|
||||
echo " Target : $path"
|
||||
echo " Method : fio randwrite bs=4k --sync=1 (O_SYNC), psync engine"
|
||||
echo " Profile : runtime=${runtime}s/run size=${size^^}"
|
||||
echo " Note : every write is a synchronous ZIL commit -- this is the load"
|
||||
echo " your SLOG actually sees. Watch it live in another shell with:"
|
||||
echo " zpool iostat -vl <pool> 1"
|
||||
echo ""
|
||||
|
||||
# T1 is the headline single-stream latency; the sweep shows how the SLOG
|
||||
# scales as concurrent sync writers (NFS/iSCSI/VM threads) pile on. Run every
|
||||
# measurement FIRST (progress goes to stderr), collect the results, then draw
|
||||
# the whole table in one block so the "measuring..." lines can't interleave
|
||||
# into the middle of the table.
|
||||
local jobs_list="1 4 8 16"
|
||||
local -a rows
|
||||
local j res iops bw p50 p99 mean
|
||||
for j in $jobs_list; do
|
||||
echo " measuring 4K sync randwrite T$j ..." >&2
|
||||
res=$(run_fio_sync 4k "$j")
|
||||
read -r iops bw p50 p99 mean <<< "$res"
|
||||
rows+=("$(slog_row "4K sync T$j" "$iops" "$bw" "$p50" "$p99")")
|
||||
done
|
||||
|
||||
rm -f "$benchfile"
|
||||
|
||||
# Render the assembled table
|
||||
echo ""
|
||||
slog_rule
|
||||
slog_row "Test" "IOPS" "MB/s" "p50 lat(us)" "p99 lat(us)"
|
||||
slog_rule
|
||||
printf '%s\n' "${rows[@]}"
|
||||
slog_rule
|
||||
echo ""
|
||||
echo " Healthy Optane SLOG (eg P1600X) single-stream (T1) target:"
|
||||
echo " ~15-25k IOPS, p50 latency ~40-65us. Much higher latency usually means"
|
||||
echo " CPU C-states / PCIe ASPM / BIOS power profile (eg Dell DAPC) throttling."
|
||||
echo ""
|
||||
}
|
||||
|
||||
function cdm_speedtest {
|
||||
# Auto-detect the best engine and whether O_DIRECT works on this path.
|
||||
detect_io_settings
|
||||
benchfile="$path/speedtest-hd.bench"
|
||||
|
||||
# Write tests
|
||||
fio_write_single_random_4k
|
||||
fio_write_parallel_random_64k
|
||||
fio_write_single_sequential_1m
|
||||
local direct_label="enabled (device)"
|
||||
[ "$direct" != 1 ] && direct_label="DISABLED (buffered -- may reflect RAM cache!)"
|
||||
|
||||
# Read Tests
|
||||
fio_read_sequential_1m
|
||||
fio_read_random_4k
|
||||
# Banner
|
||||
echo ""
|
||||
echo " speedtest-hd : CrystalDiskMark-style storage benchmark"
|
||||
echo " Target : $path"
|
||||
echo " Engine : $engine O_DIRECT: $direct_label"
|
||||
echo " Profile : size=${size^^} runtime=${runtime}s/run (8 runs)"
|
||||
echo ""
|
||||
|
||||
# The four CrystalDiskMark default tests. Q = queue depth (--iodepth),
|
||||
# T = threads (--numjobs). The first three are T1 (single thread); the last
|
||||
# is T16. A single shared test file is reused -- with numjobs>1 every job
|
||||
# issues IO against the same file, which is fine.
|
||||
# Each run now returns both MB/s and IOPS ("bw iops"), so we capture the pair.
|
||||
# label read-rw write-rw bs Q T
|
||||
echo " measuring SEQ1M Q8T1 ..." >&2
|
||||
local s8r_bw s8r_io; read -r s8r_bw s8r_io <<< "$(run_fio read 1m 8 1)"
|
||||
local s8w_bw s8w_io; read -r s8w_bw s8w_io <<< "$(run_fio write 1m 8 1)"
|
||||
echo " measuring SEQ1M Q1T1 ..." >&2
|
||||
local s1r_bw s1r_io; read -r s1r_bw s1r_io <<< "$(run_fio read 1m 1 1)"
|
||||
local s1w_bw s1w_io; read -r s1w_bw s1w_io <<< "$(run_fio write 1m 1 1)"
|
||||
echo " measuring RND4K Q32T1 ..." >&2
|
||||
local r32r_bw r32r_io; read -r r32r_bw r32r_io <<< "$(run_fio randread 4k 32 1)"
|
||||
local r32w_bw r32w_io; read -r r32w_bw r32w_io <<< "$(run_fio randwrite 4k 32 1)"
|
||||
echo " measuring RND4K Q32T16 ..." >&2
|
||||
local r1r_bw r1r_io; read -r r1r_bw r1r_io <<< "$(run_fio randread 4k 32 16)"
|
||||
local r1w_bw r1w_io; read -r r1w_bw r1w_io <<< "$(run_fio randwrite 4k 32 16)"
|
||||
|
||||
# Cleanup the shared test file
|
||||
rm -f "$benchfile"
|
||||
|
||||
# Render the CrystalDiskMark-style table
|
||||
echo ""
|
||||
tbl_rule
|
||||
tbl_row "Test" "Read (MB/s)" "Write (MB/s)" "Read (IOPS)" "Write (IOPS)"
|
||||
tbl_rule
|
||||
tbl_row "SEQ1M Q8T1" "$s8r_bw" "$s8w_bw" "$s8r_io" "$s8w_io"
|
||||
tbl_row "SEQ1M Q1T1" "$s1r_bw" "$s1w_bw" "$s1r_io" "$s1w_io"
|
||||
tbl_row "RND4K Q32T1" "$r32r_bw" "$r32w_bw" "$r32r_io" "$r32w_io"
|
||||
tbl_row "RND4K Q32T16" "$r1r_bw" "$r1w_bw" "$r1r_io" "$r1w_io"
|
||||
tbl_rule
|
||||
echo ""
|
||||
}
|
||||
|
||||
function dd_speedtest {
|
||||
# Basic HD speed test using DD
|
||||
# Basic HD speed test using dd (fallback when fio is not installed)
|
||||
# mReschke 2017-07-11
|
||||
|
||||
local file ddsize
|
||||
file=$path/bigfile
|
||||
size=1024
|
||||
ddsize=1024
|
||||
|
||||
echo "Running dd based HD/SSD/NVMe Benchmarks"
|
||||
echo "---------------------------------------"
|
||||
|
||||
printf "Cached write speed...\n"
|
||||
dd if=/dev/zero of=$file bs=1M count=$size
|
||||
dd if=/dev/zero of=$file bs=1M count=$ddsize
|
||||
|
||||
printf "\nUncached write speed...\n"
|
||||
dd if=/dev/zero of=$file bs=1M count=$size conv=fdatasync,notrunc
|
||||
dd if=/dev/zero of=$file bs=1M count=$ddsize conv=fdatasync,notrunc
|
||||
|
||||
printf "\nUncached read speed...\n"
|
||||
echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null
|
||||
dd if=$file of=/dev/null bs=1M count=$size
|
||||
dd if=$file of=/dev/null bs=1M count=$ddsize
|
||||
|
||||
printf "\nCached read speed...\n"
|
||||
dd if=$file of=/dev/null bs=1M count=$size
|
||||
dd if=$file of=/dev/null bs=1M count=$ddsize
|
||||
|
||||
rm $file
|
||||
printf "\nDone\n"
|
||||
@@ -411,40 +411,45 @@ function dd_speedtest {
|
||||
|
||||
# Show help and usage information
|
||||
function usage {
|
||||
echo "Robust Flexible Input/Output HD Speedtest"
|
||||
echo " If FIO is installed, we use FIO for more detailed performance analysis."
|
||||
echo " If FIO is not installed, we use basic DD analysis."
|
||||
echo " You should apt install fio (pacman -S fio) for detailed analysis."
|
||||
echo "speedtest-hd : CrystalDiskMark-style storage benchmark"
|
||||
echo " Runs the same four tests as CrystalDiskMark (each Read + Write):"
|
||||
echo " SEQ1M Q8T1 Sequential 1MiB, queue depth 8, 1 thread"
|
||||
echo " SEQ1M Q1T1 Sequential 1MiB, queue depth 1, 1 thread"
|
||||
echo " RND4K Q32T1 Random 4KiB, queue depth 32, 1 thread"
|
||||
echo " RND4K Q32T16 Random 4KiB, queue depth 32, 16 threads"
|
||||
echo " Uses fio if installed (recommended), else basic dd."
|
||||
echo "mReschke 2024-01-18"
|
||||
echo ""
|
||||
echo "NOTICE, this creates a 1GB file on the desired destination disk."
|
||||
echo "Please ensure you have write access with 1GB free space on destination."
|
||||
echo "NOTICE: creates one test file (default 1GB) on the destination disk."
|
||||
echo "Please ensure you have write access with enough free space."
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " This will use FIO if installed, else DD"
|
||||
echo " Auto (fio if installed, else dd):"
|
||||
echo " ./speedtest-hd /mnt/somedisk"
|
||||
echo " ./speedtest-hd ."
|
||||
echo ""
|
||||
echo " This will force FIO"
|
||||
echo " Force fio / force dd:"
|
||||
echo " ./speedtest-hd /mnt/somedisk --fio"
|
||||
echo " ./speedtest-hd . --fio"
|
||||
echo ""
|
||||
echo " This will force DD"
|
||||
echo " ./speedtest-hd /mnt/somedisk --dd"
|
||||
echo " ./speedtest-hd . --dd"
|
||||
echo ""
|
||||
echo " Add --simple (FIO only) for a compact, aligned summary of MB/s values"
|
||||
echo " ./speedtest-hd . --simple"
|
||||
echo " ./speedtest-hd . --fio --simple"
|
||||
echo " SLOG / sync-write latency profile (ZFS ZIL, NFS/iSCSI/VM sync IO):"
|
||||
echo " ./speedtest-hd /mnt/zpool/dataset --slog"
|
||||
echo " 4K synchronous writes at T1/T4/T8/T16; reports IOPS, MB/s, p50/p99"
|
||||
echo " commit latency. Requires fio + python3. (Tip: watch the SLOG with"
|
||||
echo " 'zpool iostat -vl <pool> 1' in another shell.)"
|
||||
echo ""
|
||||
echo " FIO tuning flags (all auto-detected by default, override as needed):"
|
||||
echo " Tuning flags (auto-detected by default, override as needed):"
|
||||
echo " --engine=io_uring|libaio|posixaio|sync IO engine (default: auto)"
|
||||
echo " --direct Force O_DIRECT (bypass page cache)"
|
||||
echo " --buffered Force buffered IO (eg if O_DIRECT is unsupported)"
|
||||
echo " --runtime=SEC Seconds per test (default: 30)"
|
||||
echo " --runtime=SEC Seconds per run (default: 5, like CrystalDiskMark)"
|
||||
echo " --size=SIZE Test file size (default: 1g)"
|
||||
echo " --verbose Print full fio output for every run (summary table unchanged)"
|
||||
echo " Examples:"
|
||||
echo " ./speedtest-hd /mnt/nvmepool --engine=io_uring --runtime=60"
|
||||
echo " ./speedtest-hd /mnt/nvmepool --runtime=10 --size=4g"
|
||||
echo " ./speedtest-hd /mnt/nfsshare --buffered"
|
||||
echo " ./speedtest-hd /mnt/nvmepool --verbose"
|
||||
echo " ./speedtest-hd /mnt/nvme-ultra-r10/vm-root --slog --runtime=30"
|
||||
exit 0
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user