Vendor the command set the menu and shell depend on
This commit is contained in:
Executable
+232
@@ -0,0 +1,232 @@
|
||||
#!/bin/bash
|
||||
|
||||
# blob:summary=Measure live disk read and write speed
|
||||
# blob:args=[target-dir]
|
||||
|
||||
set -e
|
||||
|
||||
if [[ -n ${1:-} && ! -d $1 ]]; then
|
||||
echo "Usage: blob-disk-speedtest [target-dir]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
target_dir="${1:-${XDG_CACHE_HOME:-$HOME/.cache}/blob}"
|
||||
phase_seconds=8
|
||||
parallel=4
|
||||
chunk_mb=4
|
||||
file_mb=256
|
||||
|
||||
mkdir -p "$target_dir"
|
||||
|
||||
worker_pids=()
|
||||
chunk_file=""
|
||||
test_files=()
|
||||
|
||||
stop_workers() {
|
||||
local pid
|
||||
for pid in "${worker_pids[@]}"; do
|
||||
[[ -n $pid ]] || continue
|
||||
pkill -TERM -P "$pid" 2>/dev/null || true
|
||||
kill "$pid" 2>/dev/null || true
|
||||
done
|
||||
for pid in "${worker_pids[@]}"; do
|
||||
[[ -n $pid ]] || continue
|
||||
wait "$pid" 2>/dev/null || true
|
||||
done
|
||||
worker_pids=()
|
||||
}
|
||||
|
||||
alive_workers() {
|
||||
local pid count=0
|
||||
for pid in "${worker_pids[@]}"; do
|
||||
kill -0 "$pid" 2>/dev/null && count=$((count + 1))
|
||||
done
|
||||
echo "$count"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
# Unlink before stopping the workers, so even a cleanup cut short by an
|
||||
# impatient SIGKILL has already taken the names off the filesystem. A live
|
||||
# write worker's next dd pass recreates its file by name, so sweep again
|
||||
# once they are gone.
|
||||
rm -f ${chunk_file:+"$chunk_file"} "${test_files[@]}"
|
||||
stop_workers
|
||||
rm -f ${chunk_file:+"$chunk_file"} "${test_files[@]}"
|
||||
}
|
||||
# Armed before any scratch file exists, so a failed preflight check below
|
||||
# cannot leak them.
|
||||
trap cleanup EXIT
|
||||
trap 'exit 143' TERM INT
|
||||
|
||||
# Exclusive per-invocation scratch files: predictable names could clobber a
|
||||
# user's file, follow a planted symlink, or let overlapping runs delete each
|
||||
# other's active files out from under the measurement. Each worker gets its
|
||||
# own on-disk file so the phases run at a queue depth the device can actually
|
||||
# stretch out on, like the network test's parallel curl workers.
|
||||
#
|
||||
# The files are marked NOCOW where the filesystem supports it (btrfs), which
|
||||
# turns off copy-on-write, checksums, and compression for them. That is what
|
||||
# makes O_DIRECT truly direct on btrfs -- with checksums on it silently falls
|
||||
# back to the page cache -- and it makes every rewrite land in place instead
|
||||
# of churning the extent allocator, which run-to-run reproducibility depends
|
||||
# on.
|
||||
chunk_file=$(mktemp /dev/shm/blob-disk-speedtest-XXXXXX.src)
|
||||
for (( i = 0; i < parallel; i++ )); do
|
||||
file=$(mktemp "$target_dir/disk-speedtest-XXXXXX.dat")
|
||||
chattr +C "$file" 2>/dev/null || true
|
||||
test_files+=("$file")
|
||||
done
|
||||
|
||||
format_rate() {
|
||||
awk -v value="$1" 'BEGIN {
|
||||
if (value <= 0) print "0.0"
|
||||
else if (value < 10) printf "%.1f\n", value
|
||||
else printf "%.0f\n", value
|
||||
}'
|
||||
}
|
||||
|
||||
# Resolve the block device backing the target directory, so throughput can be
|
||||
# sampled from its kernel I/O counters the same way the network speed test
|
||||
# samples the interface counters.
|
||||
source_dev=$(findmnt -no SOURCE --target "$target_dir" 2>/dev/null)
|
||||
source_dev=${source_dev%%\[*} # Strip btrfs subvolume suffix: /dev/sda2[/@home]
|
||||
|
||||
if [[ $source_dev != /dev/* ]]; then
|
||||
echo "Cannot find a disk behind $target_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dev=$(readlink -f "$source_dev")
|
||||
dev=${dev##*/}
|
||||
|
||||
if [[ ! -r /sys/class/block/$dev/stat ]]; then
|
||||
echo "No I/O statistics for $dev" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
available_mb=$(df --output=avail -m "$target_dir" | tail -1 | tr -d ' ')
|
||||
if (( available_mb < parallel * file_mb * 2 )); then
|
||||
echo "Need at least $((parallel * file_mb * 2))MB free on $target_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Name the physical disk under test, walking dm-crypt/LVM layers and the
|
||||
# partition table up to the whole device that carries the hardware model.
|
||||
disk=$dev
|
||||
while slave=$(ls "/sys/class/block/$disk/slaves" 2>/dev/null | head -1); [[ -n $slave ]]; do
|
||||
disk=$slave
|
||||
done
|
||||
if [[ -f /sys/class/block/$disk/partition ]]; then
|
||||
parent=$(readlink -f "/sys/class/block/$disk")
|
||||
parent=${parent%/*}
|
||||
disk=${parent##*/}
|
||||
fi
|
||||
model=$(lsblk -dno MODEL "/dev/$disk" 2>/dev/null | sed 's/^ *//; s/ *$//')
|
||||
echo "disk ${model:-$disk}"
|
||||
|
||||
# The stress data must be incompressible so nothing between the write call
|
||||
# and the flash can shrink it. Staging a urandom chunk in RAM also keeps the
|
||||
# source out of the measurement -- reading tmpfs is a memcpy.
|
||||
dd if=/dev/urandom of="$chunk_file" bs=${chunk_mb}M count=$((file_mb / chunk_mb)) status=none
|
||||
|
||||
# Workers loop only while the main script lives: if cleanup ever loses the
|
||||
# race with a kill, an orphaned worker finishes its current pass and stops
|
||||
# instead of hammering the disk forever.
|
||||
write_worker() {
|
||||
local file=$1
|
||||
while kill -0 $$ 2>/dev/null; do
|
||||
dd if="$chunk_file" of="$file" bs=${chunk_mb}M oflag=direct conv=notrunc status=none 2>/dev/null || return
|
||||
done
|
||||
}
|
||||
|
||||
read_worker() {
|
||||
local file=$1
|
||||
while kill -0 $$ 2>/dev/null; do
|
||||
dd if="$file" of=/dev/null bs=${chunk_mb}M iflag=direct status=none 2>/dev/null || return
|
||||
done
|
||||
}
|
||||
|
||||
device_sectors() {
|
||||
local -a stats
|
||||
read -r -a stats < "/sys/class/block/$dev/stat"
|
||||
if [[ $1 == "read" ]]; then
|
||||
echo "${stats[2]}"
|
||||
else
|
||||
echo "${stats[6]}"
|
||||
fi
|
||||
}
|
||||
|
||||
run_phase() {
|
||||
local phase=$1
|
||||
local file before after deadline rate alive samples=0
|
||||
local baseline_sectors baseline_time end_time
|
||||
|
||||
for file in "${test_files[@]}"; do
|
||||
"${phase}_worker" "$file" 2>/dev/null &
|
||||
worker_pids+=("$!")
|
||||
done
|
||||
|
||||
before=$(device_sectors "$phase")
|
||||
deadline=$((SECONDS + phase_seconds))
|
||||
|
||||
while (( SECONDS < deadline )) && (( $(alive_workers) > 0 )); do
|
||||
sleep 1
|
||||
after=$(device_sectors "$phase")
|
||||
end_time=$EPOCHREALTIME
|
||||
rate=$(awk -v before="$before" -v after="$after" 'BEGIN {
|
||||
if (after < before) print 0
|
||||
else print (after - before) * 512 / 1000000
|
||||
}')
|
||||
echo "$phase $(format_rate "$rate")"
|
||||
samples=$((samples + 1))
|
||||
# The first second is warm-up -- governor ramp, crypt workers spinning
|
||||
# up -- so the steady-state average starts after it.
|
||||
if (( samples == 1 )); then
|
||||
baseline_sectors=$after
|
||||
baseline_time=$end_time
|
||||
fi
|
||||
before=$after
|
||||
done
|
||||
|
||||
# The workers only stop on their own when dd fails (quota, I/O error, full
|
||||
# disk), so any worker gone before the deadline is a failed measurement,
|
||||
# not a finished one.
|
||||
alive=$(alive_workers)
|
||||
stop_workers
|
||||
if (( alive < parallel )); then
|
||||
echo "Disk $phase test failed before finishing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The figure the dial settles on is the steady-state mean over the whole
|
||||
# phase, not whatever rate the final second happened to catch.
|
||||
if (( samples > 1 )); then
|
||||
rate=$(awk -v before="$baseline_sectors" -v after="$after" -v start="$baseline_time" -v end="$end_time" 'BEGIN {
|
||||
secs = end - start
|
||||
if (secs <= 0 || after < before) print 0
|
||||
else print (after - before) * 512 / 1000000 / secs
|
||||
}')
|
||||
echo "$phase $(format_rate "$rate")"
|
||||
fi
|
||||
}
|
||||
|
||||
# The read phase runs first, so its data must be staged before any measuring
|
||||
# starts. Direct I/O leaves nothing in the page cache to serve reads from.
|
||||
for file in "${test_files[@]}"; do
|
||||
dd if="$chunk_file" of="$file" bs=${chunk_mb}M oflag=direct conv=notrunc status=none 2>/dev/null &
|
||||
worker_pids+=("$!")
|
||||
done
|
||||
|
||||
stage_failed=0
|
||||
for pid in "${worker_pids[@]}"; do
|
||||
wait "$pid" || stage_failed=1
|
||||
done
|
||||
worker_pids=()
|
||||
|
||||
if (( stage_failed )) || [[ ! -s ${test_files[0]} ]]; then
|
||||
echo "Direct disk I/O is not available on $target_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_phase read
|
||||
run_phase write
|
||||
Reference in New Issue
Block a user