More robust error handling if fio fails
This commit is contained in:
+81
-9
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user