More robust error handling if fio fails

This commit is contained in:
2026-06-21 22:35:59 -06:00
parent 4f0bb2f613
commit 706baff65c
2 changed files with 86 additions and 9 deletions
+81 -9
View File
@@ -32,6 +32,7 @@ import glob
import json
import os
import shutil
import socket
import statistics
import subprocess
import sys
@@ -179,6 +180,19 @@ def step(message: str) -> None:
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:
"""A clear, ruled header for one --verbose fio section -> stderr."""
width = 78
@@ -367,10 +381,41 @@ def run_fio(
return _aggregate(data["jobs"], _direction_of(rw))
except (json.JSONDecodeError, KeyError, ValueError):
# 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()
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:
"""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)
)
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)}"))
for line_ in extra:
print(_meta_line(line_))
@@ -694,23 +741,48 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
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.
mode = cfg.mode
if mode in ("cdm", "slog") and not have_fio:
print(ERR.paint("ERROR:", BOLD, RED)
+ " --fio/--slog require fio (apt install fio / pacman -S fio).",
file=sys.stderr)
if fio_broken:
print(ERR.paint("ERROR:", BOLD, RED)
+ 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)
return 1
if mode is None:
if have_fio:
mode = "cdm"
else:
print(OUT.paint("\nfio is not installed -- falling back to basic dd test.",
YELLOW))
print(OUT.paint("Install fio for the full CrystalDiskMark-style benchmark.",
DIM))
if fio_broken:
print(OUT.paint(f"\nfio is installed but {fio_broken} -- falling back "
"to the basic dd test.", YELLOW))
else:
print(OUT.paint("\nfio is not installed -- falling back to basic dd test.",
YELLOW))
print(OUT.paint("Install fio for the full CrystalDiskMark-style benchmark.",
DIM))
mode = "dd"
confirm(cfg)