Remove the old bash script and update README.md and CLAUDE.md

This commit is contained in:
2026-06-21 21:16:47 -06:00
parent 5561a95a94
commit 363379b2c2
4 changed files with 100 additions and 535 deletions
+62 -52
View File
@@ -1,65 +1,75 @@
# speedtest-hd
Single-file Bash CLI (`speedtest-hd.sh`) that benchmarks any mounted path (HDD/SSD/NVMe,
local or NFS/ZFS) using `fio`, presented as a CrystalDiskMark-style ASCII table. Falls back
to `dd` when fio is absent. Goal: a quick, generic, "point it at a mount" disk tester — not
a ZFS/NVMe-specific tool.
`speedtest-hd.py` — a single-file Python 3 CLI that benchmarks any mounted path
(HDD/SSD/NVMe, local or NFS/ZFS) using `fio`, presented as a CrystalDiskMark-style ASCII
table. Falls back to `dd` when fio is absent. Goal: a quick, generic, "point it at a mount"
disk tester — not a ZFS/NVMe-specific tool. `README.md` is kept in sync with the CLI.
## Usage
```
./speedtest-hd.sh <path> [--fio|--dd|--slog] [flags]
./speedtest-hd.py <path> [--fio|--dd|--slog] [flags] # or: python3 speedtest-hd.py ...
```
- Default: fio if installed, else dd. `--fio` forces CDM mode, `--slog` forces sync-latency mode.
- Flags: `--engine=io_uring|libaio|posixaio|sync` (default auto), `--direct`/`--buffered`
(default auto-probe), `--runtime=SEC` (default 5), `--size=SIZE` (default 1g), `--verbose`
(also dumps raw fio output to stderr).
- Default mode: fio if installed (→ cdm), else dd. `--fio`/`--dd`/`--slog` force a mode (mutually exclusive).
- Flags: `--engine {io_uring,libaio,posixaio,sync}` (default auto), `--direct`/`--buffered`
(default auto-probe), `--runtime SEC` (default 5), `--size SIZE` (default 1g), `--verbose`
(dump raw fio output to stderr), `-y`/`--yes` (skip confirm prompt). argparse → both
`--flag value` and `--flag=value` work.
## Modes & layout
- **CDM mode** (`cdm_speedtest`): runs CrystalDiskMark's default tests, each read+write,
prints a 5-col table (Read/Write MB/s, Read/Write IOPS). Tests: `SEQ1M Q8T1`, `SEQ1M Q1T1`,
`RND4K Q32T1`, `RND4K Q32T16`. Q = `--iodepth`, T = `--numjobs`.
- **SLOG mode** (`slog_speedtest`): 4K synchronous randwrite sweep at T1/T4/T8/T16 to profile
ZFS ZIL / SLOG (and any NFS/iSCSI/VM sync workload). Reports IOPS, MB/s, p50/p99 commit
latency. Requires `fio` + `python3` (JSON percentile parsing).
- **dd mode** (`dd_speedtest`): legacy cached/uncached read/write fallback.
## Requirements
- **python3 (3.7+)** — required for everything; uses only the standard library (argparse, json,
dataclasses, statistics). No third-party packages.
- **fio** — recommended; without it, auto mode falls back to `dd`. `--fio`/`--slog` hard-require it.
- **sudo** — fio is run via `sudo` (for O_DIRECT + device-cache flush).
## Key implementation notes
- `detect_io_settings` auto-picks the engine (io_uring → libaio → posixaio → sync) and probes
whether O_DIRECT works, via tiny throwaway `fio_probe` jobs. Falls back to buffered with a
warning if O_DIRECT is rejected (older OpenZFS <2.3, some NFS). **libaio is only truly async
with `--direct=1`** — that's why io_uring is preferred. O_DIRECT bypasses the page cache so we
measure the device, not RAM (buffered results, esp. reads, can reflect ARC/page cache).
- One shared test file `<path>/speedtest-hd.bench` is reused across all runs (laid out once),
removed at the end. Footprint = `--size` (default 1G), matching the startup notice.
- `run_fio` (CDM) returns `"MB/s IOPS"`; `run_fio_sync` (SLOG) forces `--ioengine=psync --sync=1`
(O_SYNC) so every write is a ZIL commit regardless of dataset sync property, and parses fio
**JSON** to aggregate across jobs (sum IOPS/bw, avg p50, worst-case p99). CDM mode parses
fio's text output: `fio_bw_mbps` takes the parenthetical **SI MB/s** (matches CDM's decimal
MB/s, normalizes kB/MB/GB); `fio_iops` expands k/M suffixes.
- Write tests append `--end_fsync=1` so cached writes can't inflate numbers.
- ASCII tables: cell formats and dash-segment widths must stay in sync (`tbl_*` = 18/16 dashes,
`slog_*` = 18/14). Verify alignment after editing.
## Modes / profiles (functions)
- **cdm** (`cdm_profile`): CrystalDiskMark's default tests, each read+write, 5-col table
(Read/Write MB/s, Read/Write IOPS). Tests defined as data in `CDM_TESTS` (CrystalDiskMark's
default profile, in its display order): `SEQ1M Q8T1`, `SEQ1M Q1T1`, `RND4K Q32T16`, `RND4K Q1T1`.
Q=iodepth, T=numjobs. Write runs use `end_fsync`.
- **slog** (`slog_profile`): 4K synchronous randwrite sweep at T1/T4/T8/T16 (`SLOG_THREADS`) to
profile ZFS ZIL / SLOG (and any NFS/iSCSI/VM sync workload). Reports IOPS, MB/s, p50/p99
commit latency. Forces `--ioengine=psync --sync=1` (O_SYNC) regardless of dataset sync property.
- **dd** (`dd_profile`): dependency-free fallback; sequential write + uncached read + cached(RAM)
read, timed in Python.
## Key implementation
- `detect_io_settings` auto-picks engine (`ENGINE_CANDIDATES` = io_uring→libaio→posixaio→sync) and
probes O_DIRECT support via tiny throwaway `fio_probe` jobs; falls back to buffered (with a red
banner warning) when O_DIRECT is rejected (older OpenZFS <2.3, some NFS). **libaio is only truly
async with `--direct=1`** → io_uring preferred. O_DIRECT bypasses the page cache so we measure
the device, not RAM (buffered reads can reflect ARC/page cache).
- **Everything parses fio `--output-format=json`** (robust, unit-safe metrics, no text scraping):
`run_fio``_aggregate` sums bw/IOPS across jobs, averages per-job p50 (median), takes worst-case
p99. fio is run *without* `--group_reporting`; we aggregate ourselves to avoid fio's group-merge
quirks. `FioResult` holds bw_mbps (decimal MB/s, matches CDM's SI figure), iops, p50/p99/mean (µs).
- One shared bench file `<path>/speedtest-hd.bench`, reused across all runs, removed at end
(`_cleanup`). Footprint = `--size` (default 1G).
- Output styling: `Painter`/ANSI (basic 16-color), honors `NO_COLOR`/`FORCE_COLOR`/`TERM=dumb`,
per-stream isatty. **stdout = results (banner + tables); stderr = progress + verbose dumps**, each
with its own color flag so piping one works. `render_table` auto-sizes columns on *plain* text
(color applied after padding, so escapes don't skew widths).
- `Config` dataclass holds resolved settings; `confirm` prompts unless `--yes`.
## History / decisions
- Originated from the Ars Technica fio guide. Original 4K test used `numjobs=1 iodepth=1`, which
measures single-op **latency**, not throughput — ~12 MB/s on fast NVMe is correct for QD1, not
a bug. Refactored toward parallel/deep-queue tests to show real device capability, then fully
reshaped into the CrystalDiskMark profile above. `--simple` flag was removed; the table is now
the only fio output.
- Note: the current `RND4K` rows are `Q32T1` + `Q32T16`. CrystalDiskMark's actual latest default
profile is `Q32T16` + `Q1T1` (single-queue random is the meaningful low-end number). If aligning
strictly to CDM, the `Q32T1` row should become `Q1T1` (`iodepth=1 numjobs=1`).
- Originated from the Ars Technica fio guide. Original 4K test used QD1/1-job,
which measures single-op **latency**, not throughput — ~12 MB/s on fast NVMe is correct for QD1,
not a bug. Evolved to parallel/deep-queue CDM tests, then rewritten in Python for robust JSON
parsing, color, and the SLOG profile.
- `CDM_TESTS` is aligned to CrystalDiskMark's true default profile (`Q32T16` + `Q1T1`); an earlier
iteration used `Q32T1` + `Q32T16`. The TrueNAS case study in README.md was captured with that
earlier profile — its `Q32T1` vs `Q32T16` comparison is the reason the default changed.
## SLOG performance context (why `--slog` exists)
## SLOG performance context (why `--slog` exists; full case study in README.md)
Built to diagnose TrueNAS SCALE box `linvault1` (Dell R630, Xeon E5-2680 v3; pool `nvme-ultra-r10`
= 6× KingSpec XG7000 RAID10 + Intel Optane P1600X SLOG; dataset `vm-root` sync=always). Poor sync
= 6× KingSpec XG7000 RAID10 + Intel Optane P1600X SLOG; dataset `vm-root` sync=always). "Slow" sync
writes were **CPU power management**, not the SLOG:
- Fix (biggest last): Dell BIOS profile DAPC → **Performance** (~2×); cstate kernel args; and the
big one — CPU **governor `performance`** (was `intel_cpufreq`+`schedutil`, which parked cores at
1.2 GHz because QD1 sync load blocks on the SLOG and reads as "idle"). Persist via TrueNAS Post
Init: `echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor`.
- Result: 4K sync T1 ~3,050 → 10,687 IOPS, p50 ~328 → 85µs (≈ Haswell ZIL-commit floor).
- Diagnostic: `zpool iostat -vl <pool> 1` during fio showed the Optane `logs` vdev at ~90µs
disk_wait — proving the SLOG was fine and latency was upstream (CPU).
- Healthy Optane SLOG single-stream (T1) target: ~1525k IOPS, p50 ~4065µs. Much higher usually
= C-states / PCIe ASPM / BIOS power profile throttling.
- Fixes (biggest last): Dell BIOS DAPC → **Performance** (~2×); cstate/ASPM kernel args; and the big
one — CPU **governor `performance`** (was `intel_cpufreq`+`schedutil`, parking cores at 1.2 GHz
because QD1 sync load blocks on the SLOG and reads as "idle"). Persist via TrueNAS Post Init:
`echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor`.
- Result: 4K sync T1 ~3,050 → 10,687 IOPS, p50 ~328 → 85µs (≈ Haswell ZIL-commit floor); T16 → ~78k
IOPS / 319 MB/s, scaling regression gone.
- Decisive diagnostic: `zpool iostat -vl <pool> 1` during fio showed the Optane `logs` vdev at ~90µs
disk_wait — proving the SLOG was fine and latency was upstream (CPU). Also: large sync writes
bypass the SLOG (indirect ZIL >32K) — only small 4K sync writes exercise it, which is what `--slog`
does. Healthy Optane SLOG T1 target: ~1525k IOPS, p50 ~4065µs.