iotest
Ever feel like fio is too complicated and too optimized? Well, simple-iotest covers this for you!
Using the most simple non-optimized write() or writev() with fallback to python f.write() (just like your code!) to test file system io performance.
Warning: if using big file sizes, make sure you have enough memory to hold all these bits! (Or use -bs: each worker then holds a chunk pool of up to 16 MiB and one block to write from, and a one-block read buffer, instead of whole files.) Before it starts, iotest estimates the memory the run needs: each worker's data buffers (none in index and meta passes; a read worker that finds some of its files missing also holds the content to recreate them), plus, per operation, about 80 (meta) to 140 (simultaneous) bytes for the results it keeps until the run ends, and about 70 to 110 more with -tl. If that is more than the available memory plus swap, it lowers the process count; if it is more than 90% of the available memory, it warns and waits 15 seconds before going on.
Note:
Tee_Logger is a required dependency and is installed automatically. Results are also logged to /var/log/ by default (change it with -ld, or turn it off with -nl) for future keeping.
This script tests I/O performance by creating, reading, moving, and indexing files in various modes. It allows you to measure different aspects of disk performance and log the results.
Installation
pipx install simple-iotest
Optionally install numpy, which speeds up generating the random test data:
pipx install 'simple-iotest[fast]'
Usage
Run the script with:
iotest [options] [modes]
simple-iotest [options] [modes]
simple-iotest is the same command as iotest (so uvx simple-iotest works).
Default mode is write (w) when no mode is specified.
Common Options
-fs, --file_size: Size of the test files (default: 30 MiB; suffixm,g, etc. supported).-bs, --block_size: Writes and reads each file in calls of this size (same units as-fs), so large files need little memory and small-I/O patterns can be tested. Each worker reads into one buffer of this size, and writes from one chunk pool: 16 MiB of its test data plus one block (the whole file, if that is smaller), generated once before the timed pass. So each writer holds up to 16 MiB and one block (less for smaller files), which the memory estimate counts. Each chunk is written from its offset in the file modulo the pool's span (16 MiB and one byte; 16 MiB with-u), so a file is not one block repeated: a byte of the pool comes back in the file only a whole span later, never twice within any 16 MiB, ZFS's largest recordsize. No record that storage compresses on its own (ZFS records of up to 16 MiB, btrfs's 128 KiB extents, an array's blocks) contains a repeat, so compression cannot shrink the file and inflate the write figures (-zdata still compresses). Default: unset, one call per file; a block size at least the file size is the same as unset. A short write or read is retried with the rest of its block, and a written file is synced once, after its last block. An operation is still one whole file: its time covers every block, and IOPS count files. Without-u, each worker's files hold the same data (as without-bs), so deduplicating storage may store them once; with-u, each chunk is stamped with its place in the file just before it is written, inside the timed write (without-bsthe whole file is stamped before the timer starts). The tags land on the same bytes of the pool whichever chunk writes them, so every 512-byte block has its own tag and no earlier tag is left between them, but with--verifythe previous file's CRC trailer can be, so with-z -u --verify -bsthe bytes between tags are not all zeros.-fc, --file_count: Number of files to process per worker (default: 50).-pc, -t, --process_count: Number of worker processes (default: 36).-ts, --total_size: Total size target. If one of-fs,-fcand-pcis omitted, it is calculated to fit this total (the file count first, then the file size, then the process count); it is ignored, with a warning, when all three are given.-wtpt, --write_target_throughput: Target sync write throughput (e.g.500m,2g); auto-tunes the first write pass.-rtpt, --read_target_throughput: Target sync read throughput; auto-tunes the first read pass. For simultaneous mode with both targets, the initial writer/reader split is estimated from the target ratio before tuning.-d, --directory: Directory for file operations.-ld, --log_directory: Directory for the log files (default:/var/log/).-q, --quiet: Kept for compatibility: quiet is the default.-v, --verbose: Prints per-file output.-S, --stealth: Suppresses the console output and the verbose log file. It implies-nr, so no CSV or report files are written (-tlstill writes the timeline). Worker progress digits still go to stdout unless-j -sends the JSON there (the digits then go to stderr).-z, --zeros: Uses zero-filled data instead of random.-u, --unique_files: Stamps every 512-byte block with (file, block, worker) so no two blocks of the run are identical, even with-z. By default each worker writes the same buffer to all its files, which storage with deduplication (ZFSdedup=on, NetApp, Pure, VAST, ...) can store once and report as optimistic. iotest warns when a target directory is on ZFS with dedup enabled or on NFS/SMB. Stamping adds a few ms per 30 MiB file to sync times.--verify: Makes every test file self-checking, for storage acceptance tests. Each file ends with 4 bytes holding the CRC-32 (little-endian, aszlib.crc32computes it) of all the bytes before them, and every read (read,random,comprehensiveand thesimultaneousreaders) checks it, in this run or any later one, without iotest storing anything. A mismatch fails that read withverify failed: <path>: CRC-32 mismatch (stored 0x…, computed 0x…), so it counts in the failed ops and iotest exits 1. This detects bit flips and torn or misplaced writes (a write that only partly reached the media, or data that landed in another file or at another offset):--verifyimplies-u, so no two blocks are the same and a block in the wrong place changes the CRC. A file is checked only against itself, so a whole write that was lost while an older, intact file of the same name stayed is not detected. Files written without--verifyhave no CRC and fail the check: write them with--verifytoo before reading them with it. Needs a file size of at least 28 bytes (exit 2 otherwise). CPU cost: one CRC-32 pass over every byte written and read (about 8 GiB/s per core with zlib 1.3; it depends on the zlib build and the CPU). Without-bsit runs outside the op time, before each timed write and after each timed read, so only the sync figures include it; with-bsit runs block by block inside the timed op, like-ustamping.-cl, --cleanup: Deletes the test files when the run ends. By default they are kept so a laterreadrun can reuse them (the default settings leave about 53 GiB). Before writing, iotest checks free space per filesystem and refuses to start (exit 1) if the run would not fit.-nl, --no_log: Disables log file creation.-addr, --message_end_point_address: Posts a short report of each result to this URL, as Discord-style JSON ({"content": "..."}) with aPOST. A failure to post is logged and does not change the exit code.--threshold_to_report_anomaly: Adds a warning to the report when a mode's 99th-percentile latency is more than this many times its 1st-percentile latency (default: 0, off).-bt, --benchmark_time: Seconds to spend benchmarking test data generation speed before the run (default: 5).-nr, --no_report: Disables result report creation.-tl, --timeline: Also writes the per-operation timeline,<name>_ops.csv. Off by default; with-tlit is written even with-nror-S. Costs memory per operation (see the timeline section).-j, --json PATH: Writes a JSON summary of the run toPATHafter the text report, also with-nr.-prints it to stdout instead; with-S, stdout then carries only the JSON (worker progress digits go to stderr). With throughput tuning it describes the final run. A summary that cannot be written exits 1, like other result files.
Modes
write/w: Only file writes (default).read/r: Only file reads.index/i: Create and remove temporary index folders.meta/m: Metadata rates, like mdtest. Three passes in order, each with its own start barrier: create, stat, then unlink-fcempty files per process (under<directory>/<worker>/meta/, e.g.<directory>/0000/meta/, asmeta_<i>). Results are under the keysmeta_create,meta_statandmeta_unlink, and the timeline'smodeismetawithopcreate,statorunlink. They have IOPS and latency but no bandwidth, since no data is moved;-fsand-bsdo not apply and no memory is reserved for file content. Leftover meta files of an interrupted run are removed (untimed) before the create pass, the empty meta directories (and their counter directories, if nothing else is in them) are removed after the unlink pass, and-clremoves any left behind. A file missing in the stat or unlink pass counts as a failed operation.benchmark/b: Only measures how fast the test data can be generated; no test files are written.random: Random read/write steps. Reads and writes are reported separately, asrandom_writeandrandom_read(and as separate CSV columns); both use the whole pass's wall time for the sync figures.comprehensive/c: write → move → stat → read per file. Each section ends with a note that its sync figures are estimates: the pass time is split across write/move/stat/read by their share of op time.rw/wr: Run write, then read sequentially in batch mode.simultaneous/s: Pre-writes read targets, then runs half the workers writing and half reading concurrently on separate files. Removes all temp files when done. If the layout pass cannot write every read file (including all the files of a layout worker that dies), the timed pass is skipped and the failures are reported under the result keylayout(layout_1,layout_2, ... when several passes fail): in the report asFailed layout ops: <failed> of <read files>, in the CSV as an emptylayout_timecolumn, in the JSON and the short reports. Layout writes are not timed, solayouthas no times.rwi/wri: Run write, then read, then index sequentially in batch mode.
Example:
iotest -fs 50m -fc 100 -pc 4 -d /tmp/iotest write read
Will launch 4 processes each write same random 50MiB size data to 100 separate files sequentially. Then will launch another 4 processes reading the same files.
Simultaneous read/write example:
iotest -fs 50m -fc 20 -pc 8 -d /tmp/iotest simultaneous
Test files
Each worker keeps its files in a directory of its own, named by its number padded to 4 digits whatever the process count: <directory>/0000/temp_<i>.bin, <directory>/0001/temp_<i>.bin, ... (<i> is padded to the width of -fc). With several -d directories, worker n uses directory n modulo their count. So runs at different process counts share files: a -pc 9 read reads the files a -pc 12 write left in 0000 to 0008, and -cl and the leftover hint cover only the workers the run used. meta uses <directory>/0000/meta/ and so on, and simultaneous uses <directory>/.iotest_simul_<datetime>/worker_0000/ and so on.
Upgrading from 3.69 or earlier: worker directories used to be padded to the width of the process count (0, 1, ... or 00, 01, ...). Test files left in those directories are not reused (a read run recreates its files in 0000, ...) and not removed by -cl: delete them by hand.
Measurement notes
- Page cache: after each write is synced, iotest drops the file's pages (
posix_fadvise(DONTNEED)) and before each read it does the same, so results reflect the storage. On macOS, which has noposix_fadvise,F_NOCACHEis set on every test file instead. On Windows neither exists, so reads may be served from cache. - Start delay: each pass starts at a common time, far enough ahead for the workers to start, make their file content (timed by the generation benchmark) and allocate and zero-fill their read buffers, before the timed pass. The buffer time is estimated from one allocation of up to 64 MiB, assuming parallel workers together go at most twice as fast as one; the
Read buffer alloc timeandProcess start delaylines show the estimate. - Late starts: workers wait for a common start time. If some reach it late (for example, a slow start under heavy load), the report adds a line such as
Late starts: 3 of 36 workers, up to 0.0412 s late (sync figures include the delay)and a warning is logged, even without-v. The returned result also carries them aslate_starts.
Per-operation timeline
With -tl/--timeline, iotest also writes <name>_ops.csv (next to the other result files, in the first -d directory), one row per attempted operation, so you can see SSD cache exhaustion, throttling or stalls over time. It is off by default, and -tl writes it even with -nr or -S. Columns: pass (1-based index of the pass in this run; a simultaneous run is one pass), mode (the pass's mode; the three passes of a meta run all say meta), op (write, read, move, stat, index, create or unlink), worker (the worker's name, as used for its directory: 0000, 0001, ...), file_index (1-based), start_s (seconds from the pass's common start time to the operation's start, 6 decimals; for a failed operation, when the attempt began), duration_s (6 decimals, empty for a failed operation) and error (empty on success, the error text on failure). Rows are sorted by pass, then start_s. Workers that died before reporting have no rows. Throughput tuning attempts do not record it; the final tuning run does.
Recording it does no I/O and next to no work in the timed passes: each worker appends every operation to compact arrays (21 bytes an operation) and hands them over once, after its last operation. iotest keeps them until the run ends and writes the file after the report; with the transfers in between, that takes about 70 (meta) to 110 bytes of memory per operation, on top of the 80 to 140 every run needs for its results, and the memory check counts both. For example, -pc 64 -fc 100000 meta (19.2 million operations) is estimated at about 1.5 GB for its results and 1.3 GB more with -tl.
JSON summary
--json is meant for collecting runs from many hosts (for example launched with mssh). Top level: tool, version, host, started (local time with UTC offset), options (file_size, file_count, process_count as requested, directories, modes, zeros, unique (also true with --verify), start_at, block_size as requested or null, verify, timeline), start_late_s (seconds the synchronized first pass started after --start_at, 0.0 when on time, null without --start_at or when no timed pass ran, e.g. the simultaneous layout failed), results, failed_ops and report_errors. results has one entry per result key (write, read, random_write, ..., and layout for a simultaneous layout pass that failed, with ops 0 and failed the read files it could not write), each with ops, failed, first_error, process_count (the count that ran, which the memory check can lower), total_bytes, bytes_per_s_call, bytes_per_s_sync, io_time_pct, iops_call, iops_sync, latency_s (avg, median, p1, p99, p99_9, max, in seconds; null when no op succeeded), sync_estimated, late_workers and max_late_s. Values that do not apply (bytes for index, for example) are null.
Multi-host runs
--start_at EPOCH makes every host begin its first timed pass at the same wall-clock instant (seconds since the Unix epoch, e.g. $(date -d '+1 min' +%s)), so the storage sees the combined load. Compute the time once, locally, and pass it to every host:
T=$(date -d '+2 min' +%s)
mssh node[1-8] "iotest -d /scratch/\$(hostname) --start_at $T -j /scratch/iotest-\$(hostname).json"
- Each host needs its own
-ddirectory (as$(hostname)above). On a shared directory the hosts' worker files collide, their result files can get identical names (every part of the name, including the datetime to the second, can match across a fan-out) and overwrite each other, and simultaneous mode's.iotest_simul_<datetime>trees collide. - The hosts' clocks must be in sync (NTP/chrony); iotest cannot correct for skew.
- Only the first timed pass is synchronized (for
simultaneous, the pass after its layout pass); later passes start as usual. - The time must be in the future when iotest parses its arguments, otherwise it exits with
2. Leave enough time for the file generation benchmark; if less than the usual process start delay remains when the pass is scheduled, it starts after that delay instead, a warning says how late, and the report notes it. - It cannot be combined with
-wtpt/-rtpt. - iotest prints
Waiting until <local time> for --start_at (<N.N> s)while it waits. The report starts withSynchronized start at <local time> (--start_at), and the JSONoptionscarrystart_atand the top levelstart_late_s.
Exit codes
0: the run completed and every operation succeeded.1: at least one I/O operation failed (the report shows how many and the first error; a--verifymismatch counts as a failed read), the target does not have enough free space (nothing is written), throughput tuning could not reach its target,simultaneouscould not write its read files (that pass's timed part does not run; the failures count as failedlayoutops), or a result file could not be written (the report is still printed).2: invalid arguments, e.g.-fc 0or an unparseable-fs.
Check the available arguments with -h or --help for more details.
$ iotest -h
usage: iotest [-h] [-fs FILE_SIZE] [-bs BLOCK_SIZE] [-fc FILE_COUNT] [-t PROCESS_COUNT] [-ts TOTAL_SIZE]
[-d DIRECTORY] [-ld LOG_DIRECTORY] [-q] [-v] [-S] [-nl] [-nr] [-tl] [-z] [-u] [--verify] [-j PATH] [-cl]
[-addr MESSAGE_END_POINT_ADDRESS] [--threshold_to_report_anomaly THRESHOLD_TO_REPORT_ANOMALY]
[-bt BENCHMARK_TIME] [-wtpt WRITE_TARGET_THROUGHPUT] [-rtpt READ_TARGET_THROUGHPUT] [--start_at EPOCH]
[-V]
[{read,write,random,index,meta,m,benchmark,comprehensive,simultaneous,r,w,rw,wr,i,rwi,wri,c,b,s} ...]
Test total disk bandwidth. Default mode is write (w). Comprehensive mode: write -> move -> stat -> read
positional arguments:
{read,write,random,index,meta,m,benchmark,comprehensive,simultaneous,r,w,rw,wr,i,rwi,wri,c,b,s}
The mode the script will operate in (default: write / w). COMPREHENSIVE: per-thread write ->
move -> stat -> read (what your code sees). WRITE: batched all-thread write. READ: batched
all-thread read. INDEX: creates --file_count index folders, stat each, then delete. META / M:
create, stat, then unlink --file_count empty files per process, each as its own timed pass
(keys meta_create, meta_stat, meta_unlink); -fs does not apply. RWI / WRI: run write, then
read, then index sequentially in batch mode. SIMULTANEOUS / S: layout write pass for read
files, then concurrent write+read on separate files; cleans up afterward.
options:
-h, --help show this help message and exit
-fs, --file_size FILE_SIZE
File size (default:30), defaults to mb, can specify in t(b),g(b),m(b),k(b),b
-bs, --block_size BLOCK_SIZE
Write and read each file in calls of this size instead of the whole file in one call
(default): each worker reads into one buffer of this size and writes from a pool of 16 MiB of
test data and one block (or the file size), so the file does not repeat within any 16 MiB.
Same units as -fs
-fc, --file_count FILE_COUNT
Number of files to create and read per process (default:50)
-t, -pc, --process_count PROCESS_COUNT
Number of processes to run concurrently (default:36)
-ts, --total_size TOTAL_SIZE
Total size target. If -fs/-fc/-pc is omitted, auto-calculate one missing arg to fit this total
size.
-d, --directory DIRECTORY
Directory to put the files in. Repeat -d to round-robin workers across directories
(default:<pwd>)
-ld, --log_directory LOG_DIRECTORY
Directory to put the log files in (default:/var/log/)
-q, --quiet Kept for compatibility: quiet is the default. Use -v for per-file output
-v, --verbose Verbose output
-S, --stealth Suppress verbose output and verbose log file
-nl, --no_log Do not write log files
-nr, --no_report Do not write report files
-tl, --timeline Also write <report name>_ops.csv, the start time, duration and error of every operation, even
with -nr or -S. Costs up to about 110 bytes of memory per operation
-z, --zeros Use zeros instead of random numbers. Use this if you are sure no write compression is
available. Potentially higher write accuracy.
-u, --unique_files Stamp every 512-byte block with (file, block, worker) so no two blocks of the run are
identical. Use on storage that deduplicates (iotest warns when it detects some). Adds a few ms
per 30 MiB file to sync times.
--verify End every file with a CRC-32 of its other bytes and check it on every read, in this run or a
later one, to detect bit flips and torn or misplaced writes. Implies -u. Needs a file size of
at least 28 bytes; files written without --verify fail the check
-j, --json PATH Write a JSON summary of the run to PATH after the text report. Use - for stdout (with -S,
stdout then carries only the JSON)
-cl, --cleanup Delete the test files when the run ends (default: keep them, so a later read run can reuse
them)
-addr, --message_end_point_address MESSAGE_END_POINT_ADDRESS
The end point address of the message
--threshold_to_report_anomaly THRESHOLD_TO_REPORT_ANOMALY
The threshold to report if 1 percent high is higher then 1 percent low *
<threshold_to_report_anomaly>
-bt, --benchmark_time BENCHMARK_TIME
The time in seconds to run the benchmark for file generation speed
-wtpt, --write_target_throughput WRITE_TARGET_THROUGHPUT
Target sync write throughput (e.g. 500m, 2g). Auto-tunes the first write pass.
-rtpt, --read_target_throughput READ_TARGET_THROUGHPUT
Target sync read throughput (e.g. 500m, 2g). Auto-tunes the first read pass.
--start_at EPOCH Start the first timed pass at this time, in seconds since the Unix epoch (e.g. $(date -d '+1
min' +%s)), so several hosts start together. Clocks must be in sync
-V, --version show program's version number and exit
Metadata
Release files for simple-iotest 3.70.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| simple_iotest-3.70.0.tar.gz | 119.6 kB | Without provenance | Details for simple_iotest-3.70.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| simple_iotest-3.70.0-py3-none-any.whl | Python 3 | none | any | Details for simple_iotest-3.70.0-py3-none-any.whl |
Total release size: 179.0 kB
Release files / simple_iotest-3.70.0.tar.gz
| Download URL | simple_iotest-3.70.0.tar.gz |
|---|---|
| Size | 119.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
54dce50396a9138bd674b2422b9f197a94ecd2acadf41d022e443830f6c4019d
|
|
BLAKE2b-256 checksum How to use checksums |
ba4b83af50f8c1e935e0cf40e8dead6cebda39a571b1f5d6a721449fcb680265
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / simple_iotest-3.70.0-py3-none-any.whl
| Download URL | simple_iotest-3.70.0-py3-none-any.whl |
|---|---|
| Size | 59.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f6923b636968fe80b5d42ac0d30620ed1e0143eff1491b883c66833ae41e99a8
|
|
BLAKE2b-256 checksum How to use checksums |
a1f3271439464d58054c68ee09fd791bdc9d2df108bf7d1b68500f73fe698af7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for simple-iotest 3.70.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.