| Check / convenience | stutools / spit 2.0 | fio | dd | sysbench fileio |
|---|---|---|---|---|
| No config file required (yes is a plus!) | Yes | Yes | Yes | Yes |
| Mix, order, block size, queue, workers and passes in one compact argument | Yes: ws1k4q32j4x1 |
Separate options | Copy options; no mixed workload engine | Separate options |
| Create, write and verify a scratch file in one command | Yes: add -v |
Yes, verification options | No built-in data verification | Prepare, then run with validation |
| Probe io_uring and automatically fall back to libaio on Linux | Yes | Select an I/O engine | No async engine selection | Select an I/O mode |
| Worker hot-path heap-allocation count in the run summary | Yes: audited every run | Not a standard summary field | Not a standard summary field | Not a standard summary field |
| Report throughput and IOPS together | Yes | Yes | Throughput; no IOPS summary | Yes |
Spit's party trick is a small command that does useful work. This is a feature comparison, not a claim that it beats fio at everything: fio has a much broader engine and analysis toolbox. Sources: fio's manual, sysbench fileio, and GNU dd.
When you find yourself in blocks of trouble,
many devs come to me,
seeking words of wisdom,
don't write in C...
spit is Stu's powerful I/O tester: describe a storage workload, run it across workers, measure throughput, and check what was written. Version 2.0 introduces an independent Rust implementation, with Tokio coordinating workers, reusable memory pools and no successful worker-loop heap allocations. The original C suite lives on as spitc, alongside fsfiller and the other utilities.
The 2.0.0 release is not yet published on the
GitHub releases page, so build
from source until it is. The packages it will provide, for Debian, Ubuntu, RHEL,
Fedora, macOS and Windows, are described in the installation guide.
Linux stutools installs Rust as /usr/bin/spit; the optional stutools-c
package supplies /usr/bin/spitc, fsfiller and the remaining C tools. These are
upstream GitHub packages, not packages in the official distro repositories.
Start with a disposable file. Writes overwrite data inside the selected range. The tool is called spit; that is not a backup strategy.
# Create a 16 MiB file, write it once with four workers, verify every write.
spit -f scratch.img -G 16MiB -c ws1k4q32j4x1 -v
# Read the same file in random order for five seconds.
spit -f scratch.img -c rs0k4q32j4 -t 5
# A random 50:50 mix, explicitly using Linux libaio.
spit -f scratch.img -c rws0k4q32j8 -t 10 --backend libaioOn Windows use .\spit.exe from the extracted directory. The first two examples
also work on macOS and Windows; both use direct (unbuffered) I/O by default.
Windows drive roots such as E: and E:\ name a filesystem, not a test file.
-F takes a file prefix and creates PREFIX.0001, PREFIX.0002, etc.,
one file per worker. Initialize disposable files before reading them:
spit -F E:\spit-test -G 1MiB -c ws1x1 -v
spit -F E:\spit-test -G 1MiB -c rs0 -t 10 -s 0.1For a non-destructive raw volume read, run as Administrator:
spit -f \\.\C: -c rs0 -G 1GiB. Keep the two leading backslashes and omit
the trailing backslash. Spit reports the filesystem and sector size; raw Windows
writes are currently unsupported. A virtual filesystem may provide files without
exposing a raw block device; use -F in that case.
-G 1M tests a decimal megabyte; -G 1 tests a decimal gigabyte. Neither shrinks
an existing file. Use a new prefix when comparing small and large files.
Zero IOPS with requests in flight means the OS/driver has not completed an I/O
within the measured window. Synchronous calls can delay exit beyond -t;
spit explains the pending requests and excludes late completions from rates.
The compact workload ws1k4q32j4x1 means writes, s1 sequential,
k4 = 4 KiB, q32 queue depth, j4 workers splitting the range, and
x1 = one worker-range worth of bytes per worker. Quoted workloads may contain
spaces: -c "w s1 k4 q32 j4 x1". X1 instead means one position-table pass.
spit --help explains the tokens and shows the detected default backend.
man spit is the full reference. Output reports completed operations, elapsed
seconds, MiB/s, IOPS, verification counts and the hot-path allocation count.
For camera-style recording with background preallocation, retention and flushed minute/hour logs:
spit -F recordings/camera --cctv 100,5,24 -c k64 --filesizemb 16 -t 7dThis simulates 100 cameras at 5 Mbit/s each, using 64 KiB sequential writes into
16 MiB files and retaining 24 hours. -s defaults to one second. A completed
seven-day run produces 168 hourly data rows in cctv-test-<UTC time>-cameras=N.hour, with
average/max write latency in milliseconds. See the CCTV guide
for Linux/Windows examples, ownership rules and allocation behavior.
Direct I/O is the default on every platform: O_DIRECT on Linux and FreeBSD,
F_NOCACHE on macOS and FILE_FLAG_NO_BUFFERING on Windows. On Linux, direct
I/O uses io_uring when queue creation succeeds, otherwise libaio. You can
select io_uring, libaio, direct or pagecache (the OS page cache) explicitly.
--backend auto picks the platform's own default: io_uring or libaio on Linux,
pagecache on macOS and FreeBSD, and direct on Windows. Explicit backend choices
never silently fall back. A synchronous backend executes one I/O at a time per
worker; its q controls reusable batch storage.
File-system filling is a separate mode: spit --fsfiller -F DIR fills DIR with a
tree of files from 1 KiB to 16 MiB, using 150 writer threads by default. Its
options, including --writes, --synconclose, --cantstopthis and --ratelimitmbps,
are in man spit.
The automatic memory budget is half the available RAM, capped at 64 GiB
(half physical RAM on macOS; 512 MiB fallback if detection fails).
Linux cgroup-v2 limits are respected. --memory SIZE overrides this budget;
errors give a copyable value to use. This is not an RSS limit.
Stacks, Tokio, kernel queues and OS page cache are additional.
Memory is prepared before the worker clock starts and reused during I/O.
Progress uses C's [seconds / workers / in-flight] read ... MB/s (... IOPS / bytes), write ..., total ... GiB layout. It appears every second; -s 0.1 changes the
interval and -s 0 disables it. Workload R42 overrides the global random seed, for example
-c wk4096s0j32G400R42 uses 4 MiB blocks, 32 workers and a 400 GiB range.
Timed progress and averages end at -t; pending I/O drains silently afterward.
Late completions and resource cleanup do not extend the measured window.
The asynchronous completion-stall limit is -T 3600, matching C. Use -T 30
for a shorter limit; a failed run does not establish a device's bandwidth limit.
If Linux disables io_uring, an administrator can enable it with:
sudo sh -c 'echo 0 > /proc/sys/kernel/io_uring_disabled'
spit --helpValue 0 enables the kernel interface; sandbox/security restrictions can still
prevent queue creation. The command changes the running host's policy. The help
reports whether spit can actually use it. Plain sudo echo ... > ... does not
elevate the shell redirection.
In three two-second RAM-device runs at 64 workers, Rust io_uring reached median 968k read / 870k write IOPS, versus C libaio's 403k / 395k, with less process CPU. Rust libaio also beat C in sequential tests. Random mixed I/O is less flattering: io_uring needs more CPU, and some workloads trail C. Page-cache numbers are not disk numbers. Read the measurements and limitations.
"Premature optimization is the root of all evil." - Donald Knuth, Structured Programming with go to Statements
We measured anyway. The buffer is fixed; expectations may require tuning.
Rust supports the common C workload syntax, multiple targets/workers, ranges, variable block sizes, queue/submission limits, position modes, pass/time/rate limits, preconditioning, verification and metadata/unique-seed modes. Linux runs all 26 shared C safety/deep regressions as well as Rust tests and disposable block-device checks.
2.0 is not a complete C drop-in replacement. Rust's data and simple position
file formats differ from C's; C spitchecker cannot verify Rust-written data.
NUMA policies, copy/trim/coverage/GC modes, changing rate schedules, position
output, benchmark logs and latency histograms remain C workflows. Unsupported
options fail explicitly. Use spitc for those features, and check
the compatibility reference before replacing existing scripts.
cd rust
cargo build --release --locked
cargo test --locked
cargo fmt --check
cargo clippy --locked --all-targets -- -D warningsRust 1.85+ is required. Linux also needs a C linker and libaio development files;
rust/install-deps.sh installs distro prerequisites. See the Rust guide
for native macOS/Windows build requirements and offline sources.
FreeBSD also builds Rust spit with Cargo and the portable C backend
with CMake. GitHub Actions tests both implementations in real BSD VMs, alongside
native Linux/macOS/Windows and Linux distribution container builds.
The repository has two independent trees: rust/ for spit 2.0, and
c/ for the original suite (make -C c). The top-level Makefile delegates
to Rust. make -C rust dist builds/tests and creates a native package; Debian
recipes live in rust/debian/, the RPM spec and platform packaging in
rust/packaging/. make -C c dist builds the companion C package.
"There is no Rule 6." - Rob Pike, Notes on Programming in C
Bring back Pluto as a planet.