Understanding a Run¶
What a run does¶
- Fill: writes generation 1 over the whole range. Every sector
devsoak writes carries a 32-byte header (magic
DSOK, run seed, 64-bit LBA, per-sector generation count, writer id, transfer length, checksum) and an xorshift32 payload — everything is recomputable from (sector, generation), so verification is stateless. - Initial audit: sequential read-verify of the range.
- Optional
-XHD_SCSICMD phase (INQUIRY, TEST UNIT READY, READ CAPACITY vs TD_GETGEOMETRY, READ(10) vs CMD_READ byte-compare, unsupported-opcode autosense) and-Rremovable phase (three TD_ADDCHANGEINT handlers, an eject/insert cycle, stale-ETD checks, TD_REMCHANGEINT delivery-stop proof; the range is refilled after). - Soak: N worker tasks each keep
qrequests in flight — 45% writes, 45% read-verifies, 10% housekeeping — across every command dialect the driver was probed to support (CMD, ETD, TD64, NSD64, NSD-ETD64), buffer alignment variants (long/word/odd/4 KB-crossing), and an AbortIO probe roughly every thousand ops. A stripe-semaphore scheme makes overlapping traffic a defined last-writer-wins model. An auditor task re-sweeps the range every-Aminutes, and the invariant task runs the edge-case matrix continuously (see below). Main prints a status line every 10 s: ops/s, MB/s, in-flight count, per-class p50/p99 latency, error count. - Final audit, verdict, pinned-behaviour summary.
The invariant matrix¶
A dedicated task runs named edge-case tests every few seconds for the
whole run, ordered by risk tier (tier 3 only under -Z), announcing each
tier-2/3 command on the serial sink before its first issue. It covers
bounds tests (last-sector, past-end, straddle with io_Actual clamping),
zero and unaligned lengths/offsets, over-MaxTransfer, the
NSCMD_DEVICEQUERY listed-vs-implemented cross-check, an undersized-query
overrun check, stale ETD counts, 64-bit high-word garbage, CMD_STOP/START
gating, TD_MOTOR, lifecycle open/close probes, and — with -B — the
4 GB straddle and 32-bit boundary-read tests.
Bounds probes at the device end auto-select a 64-bit dialect when the offset needs one (a 32-bit probe at a >4 GB offset would truncate and false-fail), and skip when the driver has no 64-bit dialect at all.
Reading a failure report¶
A data mismatch prints, in order: the failing sector and its class — each class maps to a driver bug family:
| class | likely cause |
|---|---|
| header corrupt (not devsoak data) | data from outside the run landed here |
| wrong sector (offset bug) | offset arithmetic, ignored high word, off-by-one |
| stale generation (lost write) | lost write, reordering, cache not written back |
| payload mismatch (partial transfer) | partial DMA, MaxTransfer truncation, byte lanes |
Then the sector's decoded header claim (LBA/generation/writer/xfer_len) versus what was expected, the first 16 differing bytes as expected/read1/read2 — the buffer is prefilled 0xA5 before the first read and 0x5A before the diagnostic re-read, so "read1==A5, read2==5A" means the driver never touched the buffer — and a dump of the op ring buffer (the last 4096 ops with timestamps, worker ids, commands, offsets and results); with several workers the interleaving is usually the bug. Guard bytes (64 × 0xCC around every buffer) catch overruns and name the offending command.
The affected worker stops; the others continue unless -e was given.
Pinned behaviours¶
Where known-good drivers legitimately disagree, devsoak does not hard-fail: it pins the first observed behaviour and fails only if the behaviour changes during the run. Pinned values are printed live and summarised at the end. Observed so far:
| behaviour | scsi.device 47.4 | lide.device 40.12 | trackdisk.device 47.14 |
|---|---|---|---|
| zero-length read | success, actual 0 | IOERR_BADLENGTH | success, reads a whole sector (actual 512)! |
| non-sector-multiple length (read) | IOERR_BADLENGTH | serviced | — |
| unaligned io_Offset (read) | serviced | serviced | — |
| TD_MOTOR io_Actual | always 0 (no prev state) | always 0 | previous state |
| ETD_ dialect | IOERR_NOCMD | works | works (native) |
| TD_RAWREAD/RAWWRITE | IOERR_NOCMD | IOERR_NOCMD | native |
| plain TD_FORMAT (whole track) | works | IOERR_BADADDRESS* | works |
| bad-unit OpenDevice error | 50 | TDERR_BadUnitNum (32) | TDERR_BadUnitNum |
| IOF_QUICK | never honoured | never honoured | never honoured |
* lide.device 40.12 reads stale io_Actual as an offset high word for
TD_FORMAT, and advertises ETD_FORMAT without dispatching it — both
diagnosed to source and reported upstream; see
the Quirks File (lide-format-etd-nocmd).
Zero-length probes hard-fail only when bytes beyond the claimed io_Actual are touched. Two drivers can both be "correct" and pin differently — that's what the quirks file records, and what fingerprints capture per driver version.
Run profiles¶
- Smoke —
-t 30s -y: fill, 30 s of traffic, audit. CI on every commit. - Soak —
-t 8h -w 6 -q 8 -A 15 -W 60: overnight. - Bisect —
-w 1 -q 1 -s SEED: single request in flight, one PRNG — the op sequence is reproducible from the seed. - Big —
-Bwith-rplaced across the 4 GB boundary. - Removable —
-R, optionally-H "cmd"to script eject/insert. - SCSI —
-Xfor drivers implementing HD_SCSICMD.
Worked examples¶
All examples assume a scratch range — devsoak destroys it. Sector 0 of a
partitioned disk holds the RDB; put -r somewhere expendable.
Characterise an unknown driver in a minute. The quickest way to learn what a driver actually does — which dialects it speaks, how it answers the edge cases — is a short run in driver-under-test mode and a read of the pin lines:
devsoak lide.device 0 -d -r 512,2K -t 60s -K -y
The matrix: pinned ... lines are the driver's behavioural fingerprint
(zero-length handling, unaligned offsets, error codes for past-end and
bad units, IOF_QUICK, motor semantics...). Add -Z to also fingerprint
the risky tier (FORMAT variants, random command numbers, unadvertised
64-bit dialects).
Overnight soak — the actual endurance question. Size the watchdog for the queue depth (see CLI Reference) and let the auditor sweep periodically:
devsoak copperhf.device 0 -d -r 512,120K -t 8h -w 4 -q 6 -A 15 -W 60 -X -y
Reproduce and bisect a failure. A soak failure prints the run's seed. Replay the identical op sequence with one request in flight:
devsoak copperhf.device 0 -d -r 512,120K -w 1 -q 1 -s 1535717554 -y
-w 1 -q 1 is strictly sequential and deterministic from the seed, so
the op ring of two runs matches byte for byte — then shrink -t/-r
until the failure is minutes away instead of hours.
Hunt a crash in a fragile driver. Tiered ordering plus breadcrumbs means a lockup names its own culprit:
devsoak old.device 0 -d -r 2K,8K -t 5m -K -Z -P RAM:crumbs -o both -y
Every tier-2/3 command is announced on serial and appended to the -P
file before its first issue. After the reboot:
devsoak old.device 0 -P RAM:crumbs --resume
prints the last breadcrumb and a ready-to-paste status suspected quirks
entry for the command that was in flight. See
Surviving a crash for more.
Big device / 4 GB boundary. Place the range across the boundary (sector 8388608 at 512-byte sectors) so straddling transfers are real writes:
devsoak lide.device 0 -d -r 8388096,1K -t 60s -B -y
Catches ignored offset high words (the classic ">4 GB wraps to a low LBA" corruption) via both the boundary tests and the content model: a wrapped write lands with the wrong LBA in its sector header.