More robust error handling if fio fails
This commit is contained in:
@@ -4,6 +4,11 @@ A robust, CrystalDiskMark‑style storage benchmark for Linux, built on [`fio`](
|
|||||||
|
|
||||||
It runs the same four tests CrystalDiskMark does, plus a dedicated **SLOG / sync‑write latency profile** for diagnosing ZFS ZIL performance (NFS / iSCSI / VM sync workloads). It auto‑detects the best IO engine and whether `O_DIRECT` works on the target, and falls back to a basic `dd` test when `fio` isn't installed.
|
It runs the same four tests CrystalDiskMark does, plus a dedicated **SLOG / sync‑write latency profile** for diagnosing ZFS ZIL performance (NFS / iSCSI / VM sync workloads). It auto‑detects the best IO engine and whether `O_DIRECT` works on the target, and falls back to a basic `dd` test when `fio` isn't installed.
|
||||||
|
|
||||||
|
## Quick Install
|
||||||
|
```bash
|
||||||
|
wget https://git.mreschke.net/mreschke/speedtest-hd/raw/branch/master/speedtest-hd.py && chmod a+x speedtest-hd.py
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|||||||
+75
-3
@@ -32,6 +32,7 @@ import glob
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import socket
|
||||||
import statistics
|
import statistics
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -179,6 +180,19 @@ def step(message: str) -> None:
|
|||||||
log(f" {ERR.paint('▶', BOLD, CYAN)} {ERR.paint(message, DIM)}")
|
log(f" {ERR.paint('▶', BOLD, CYAN)} {ERR.paint(message, DIM)}")
|
||||||
|
|
||||||
|
|
||||||
|
# Messages already shown by warn(); fio tends to fail the same way on every run,
|
||||||
|
# so we de-duplicate to avoid printing the identical error 8 times.
|
||||||
|
_warned: set[str] = set()
|
||||||
|
|
||||||
|
|
||||||
|
def warn(message: str) -> None:
|
||||||
|
"""A de-duplicated warning line -> stderr (shown even without --verbose)."""
|
||||||
|
if message in _warned:
|
||||||
|
return
|
||||||
|
_warned.add(message)
|
||||||
|
log(f" {ERR.paint('!', BOLD, YELLOW)} {ERR.paint(message, YELLOW)}")
|
||||||
|
|
||||||
|
|
||||||
def vsection(title: str) -> None:
|
def vsection(title: str) -> None:
|
||||||
"""A clear, ruled header for one --verbose fio section -> stderr."""
|
"""A clear, ruled header for one --verbose fio section -> stderr."""
|
||||||
width = 78
|
width = 78
|
||||||
@@ -367,10 +381,41 @@ def run_fio(
|
|||||||
return _aggregate(data["jobs"], _direction_of(rw))
|
return _aggregate(data["jobs"], _direction_of(rw))
|
||||||
except (json.JSONDecodeError, KeyError, ValueError):
|
except (json.JSONDecodeError, KeyError, ValueError):
|
||||||
# fio failed or produced no parseable JSON; report zeros rather than
|
# fio failed or produced no parseable JSON; report zeros rather than
|
||||||
# crashing the whole run (verbose mode above shows what went wrong).
|
# crashing the whole run, but surface *why* so an all-zero table isn't
|
||||||
|
# silent. fio writes its real error to stderr (engine not loadable,
|
||||||
|
# permission denied, io_uring disabled, etc.).
|
||||||
|
detail = proc.stderr.strip() or proc.stdout.strip() or "(no output)"
|
||||||
|
first = detail.splitlines()[0] if detail else "(no output)"
|
||||||
|
warn(f"fio failed (exit {proc.returncode}): {first}")
|
||||||
return FioResult.zero()
|
return FioResult.zero()
|
||||||
|
|
||||||
|
|
||||||
|
def fio_health() -> tuple[bool, str]:
|
||||||
|
"""Check that fio can actually *run* on this host before we rely on it.
|
||||||
|
|
||||||
|
Some packaged/Homebrew fio builds are compiled for a newer CPU baseline
|
||||||
|
(e.g. x86-64-v3 / AVX2) than the machine has, so fio dies with SIGILL
|
||||||
|
("illegal hardware instruction") the instant it starts -- which would
|
||||||
|
otherwise show up as a silent table of zeros. ``fio --version`` exercises
|
||||||
|
that same startup path without needing sudo or touching the disk.
|
||||||
|
|
||||||
|
Returns ``(ok, detail)``; ``detail`` is fio's version on success or a short
|
||||||
|
failure description (a negative return code means killed by signal N).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(["fio", "--version"], capture_output=True, text=True)
|
||||||
|
except OSError as exc: # e.g. ENOEXEC on a bad binary
|
||||||
|
return False, str(exc)
|
||||||
|
if proc.returncode == 0:
|
||||||
|
return True, proc.stdout.strip() or "ok"
|
||||||
|
if proc.returncode < 0: # killed by a signal
|
||||||
|
sig = -proc.returncode
|
||||||
|
names = {4: "SIGILL (illegal instruction)", 6: "SIGABRT", 11: "SIGSEGV"}
|
||||||
|
return False, f"crashed: {names.get(sig, f'signal {sig}')}"
|
||||||
|
detail = (proc.stderr.strip() or proc.stdout.strip() or "").splitlines()
|
||||||
|
return False, f"exit {proc.returncode}" + (f": {detail[0]}" if detail else "")
|
||||||
|
|
||||||
|
|
||||||
def fio_probe(path: str, engine: str, direct: bool) -> bool:
|
def fio_probe(path: str, engine: str, direct: bool) -> bool:
|
||||||
"""Throwaway 1s fio job to see if an (engine, O_DIRECT) combo works here.
|
"""Throwaway 1s fio job to see if an (engine, O_DIRECT) combo works here.
|
||||||
|
|
||||||
@@ -442,6 +487,8 @@ def banner(title: str, cfg: Config, extra: Sequence[str] = ()) -> None:
|
|||||||
+ OUT.paint("│", BOLD, CYAN)
|
+ OUT.paint("│", BOLD, CYAN)
|
||||||
)
|
)
|
||||||
print(OUT.paint("╰" + "─" * inner + "╯", BOLD, CYAN))
|
print(OUT.paint("╰" + "─" * inner + "╯", BOLD, CYAN))
|
||||||
|
print(_meta_line(f"Host : {OUT.paint(socket.gethostname(), CYAN)}"))
|
||||||
|
print(_meta_line(f"Date : {OUT.paint(time.strftime('%Y-%m-%d %H:%M:%S %Z'), CYAN)}"))
|
||||||
print(_meta_line(f"Target : {OUT.paint(cfg.path, CYAN)}"))
|
print(_meta_line(f"Target : {OUT.paint(cfg.path, CYAN)}"))
|
||||||
for line_ in extra:
|
for line_ in extra:
|
||||||
print(_meta_line(line_))
|
print(_meta_line(line_))
|
||||||
@@ -694,18 +741,43 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||||||
benchfile=os.path.join(path, "speedtest-hd.bench"),
|
benchfile=os.path.join(path, "speedtest-hd.bench"),
|
||||||
)
|
)
|
||||||
|
|
||||||
have_fio = shutil.which("fio") is not None
|
fio_path = shutil.which("fio")
|
||||||
|
have_fio = fio_path is not None
|
||||||
|
|
||||||
|
# fio may be installed yet non-functional (e.g. a build compiled for AVX2 on
|
||||||
|
# a CPU without it dies with SIGILL). Verify it actually runs, so we fall back
|
||||||
|
# or error clearly instead of emitting a silent table of zeros.
|
||||||
|
fio_broken = ""
|
||||||
|
if have_fio:
|
||||||
|
ok, detail = fio_health()
|
||||||
|
if not ok:
|
||||||
|
have_fio = False
|
||||||
|
fio_broken = detail
|
||||||
|
warn(f"fio is installed ({fio_path}) but {detail}.")
|
||||||
|
warn("This is usually an fio build compiled for a newer CPU than this")
|
||||||
|
warn("host (often a Homebrew/AVX2 binary on an older Atom/Celeron). Check")
|
||||||
|
warn("'which fio'; prefer your distro's package (dnf/apt install fio).")
|
||||||
|
|
||||||
# Resolve the mode: explicit flag wins; otherwise fio if available, else dd.
|
# Resolve the mode: explicit flag wins; otherwise fio if available, else dd.
|
||||||
mode = cfg.mode
|
mode = cfg.mode
|
||||||
if mode in ("cdm", "slog") and not have_fio:
|
if mode in ("cdm", "slog") and not have_fio:
|
||||||
|
if fio_broken:
|
||||||
print(ERR.paint("ERROR:", BOLD, RED)
|
print(ERR.paint("ERROR:", BOLD, RED)
|
||||||
+ " --fio/--slog require fio (apt install fio / pacman -S fio).",
|
+ f" --fio/--slog need a working fio, but fio {fio_broken} on this"
|
||||||
|
+ " host. Reinstall fio, or use --dd for the dependency-free test.",
|
||||||
|
file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(ERR.paint("ERROR:", BOLD, RED)
|
||||||
|
+ " --fio/--slog require fio (dnf install fio / apt install fio).",
|
||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
if mode is None:
|
if mode is None:
|
||||||
if have_fio:
|
if have_fio:
|
||||||
mode = "cdm"
|
mode = "cdm"
|
||||||
|
else:
|
||||||
|
if fio_broken:
|
||||||
|
print(OUT.paint(f"\nfio is installed but {fio_broken} -- falling back "
|
||||||
|
"to the basic dd test.", YELLOW))
|
||||||
else:
|
else:
|
||||||
print(OUT.paint("\nfio is not installed -- falling back to basic dd test.",
|
print(OUT.paint("\nfio is not installed -- falling back to basic dd test.",
|
||||||
YELLOW))
|
YELLOW))
|
||||||
|
|||||||
Reference in New Issue
Block a user