# iozone3 3.506 (iozone.org, meta-oe recipes-benchmark) -- the IOzone filesystem # benchmark, plus its companion fileop. It is the first benchmark on this image # that leaves the process: dhrystone is integer CPU, whetstone scalar f64, # linpack dense linear algebra, nbench a ten-workload mix, libc-bench the libc # and mbw the linear-memory copy path. iozone measures STORAGE, which here means # the virtiofs cloud rootfs and the browser host behind it. # # The sweep is bounded to 64-128 kB files at 4-8 kB records: four points, enough # to exercise every operation at more than one size without writing tens of MiB # to a cloud filesystem. Guest memory is not returned to the kernel on process # exit (brintos/linux#25), so a suite that pointed iozone at the hundreds of MiB # it is normally given would starve everything scheduled behind it in this boot. # # BST-TIMEOUT: 300 # # NOTHING HERE ASSERTS A THROUGHPUT NUMBER, and that is deliberate. The rate this # machine achieves is a property of the host it runs on, not of the port -- the # same image reads at a different speed in a different browser on a different # day. What IS a property of the port is that the run completes, that every # operation iozone claims to measure produced a cell, and that no cell is zero, # infinite or NaN. So the pass criterion is a complete, well-formed table, # asserted structurally. # # THE WORK DIRECTORY IS THE SUBJECT, NOT AN INCIDENTAL, AND /tmp IS THE WRONG ONE. # The kernel mounts a tmpfs over /tmp for session-local scratch -- "wasm32: #56 # mounted tmpfs -> /tmp" in the boot log, not from /etc/fstab, so it is invisible # in the staged rootfs and appears only on a running machine. `mktemp -d` lands # there. A sweep run on that tmpfs measures RAM, never touches the cloud # filesystem, and still prints a full, healthy table at plausible-looking rates: # the exact shape of false green this file exists to prevent, and one an earlier # draft of it walked into. So the work directory is chosen from candidates that # resolve to the / mount in /proc/mounts, /tmp is asserted NOT to be one of them, # and the suite refuses to measure anything if no such directory can be had. # For scale, measured 2026-08-16 at 64 kB/4 kB: 727 kB/s write on the virtiofs # root against 43867 kB/s on that tmpfs, a ~60x gap. Measuring the wrong one is # not a rounding error. # # EVERY iozone INVOCATION IS HANDED A TARGET THAT ALREADY EXISTS, and the sweep # is run with -w. Both are workarounds for the platform, not for iozone: its # first act on a target is open(path, O_CREAT|O_WRONLY, mode 0) followed by # ftruncate -- its own "Sanity check", iozone.c:7824 -- and the virtio-fs backend # refuses to reopen the mode-0000 file that leaves behind. See the two attributed # reds at the foot of this file, which assert the unworkaround'd behaviour # positively so they go green on their own once the backend is fixed. # # A ZERO CELL WOULD NOT MERELY LOSE A NUMBER: iozone divides its elapsed time # into the kB/s column, so a stopped clock prints "inf" rather than 0 and the # table still looks like a measurement. That is the dhrystone trap one level on # -- dhrystone cannot time itself here at all, because times(2) reads 0 for a # live task (brintos/linux#38). iozone times with gettimeofday() instead # (time_so_far(), iozone.c:7328), which is host-backed and advances mid-run. . "$BST_LIB" require iozone iozone3/present # fileop ships in the same package, so its absence is a packaging failure rather # than a legitimately-absent tool -- `check`, not `require`. check iozone3/fileop-present test -x /usr/bin/fileop # --- the work directory is on the virtiofs root ------------------------------ # The longest mount point in /proc/mounts that is a prefix of $1, and its type. mount_of() { awk -v p="$1" ' { mp = $2; n = length(mp) if (substr(p,1,n) == mp && (n == 1 || length(p) == n || substr(p,n+1,1) == "/") && n >= best) { best = n; mp_best = mp; ty = $3 } } END { printf "%s %s", (mp_best == "" ? "?" : mp_best), (ty == "" ? "?" : ty) }' /proc/mounts } # The root is virtiofs, and saying so is the premise every rate below rests on. rty=$(awk '$2 == "/" { print $3; exit }' /proc/mounts) check_eq iozone3/rootfs-is-virtiofs virtiofs "$rty" # /var/tmp and /var/log are symlinks into a /var/volatile that nothing mounts on # this image, and /root does not exist -- /home/root is the home directory. $HOME # is "/" under the run-machine CLI (pid 1 is /bin/sh, so no login sets it), which # is on the right mount but is not a place to write, so it is not a candidate. W= for d in /home/root /var/lib /usr/lib/brintos-tests; do [ -d "$d" ] || continue c="$d/.bst-iozone3.$$" mkdir -p "$c" 2>/dev/null || continue cm=$(mount_of "$c") if [ "${cm%% *}" = "/" ]; then W=$c; break; fi rmdir "$c" 2>/dev/null done if [ -n "$W" ]; then bst_ok iozone3/workdir-on-virtiofs-root else bst_notok iozone3/workdir-on-virtiofs-root \ "no writable directory on the / mount -- measuring the /tmp tmpfs instead would be a false green, so nothing was measured" exit 0 fi # The negative control for the case above. If every path on this image resolved # to /, "the work directory is on /" would be true by construction and would # prove nothing -- so the tmpfs the kernel mounts over /tmp must be visible as a # separate mount. This is the case that would have caught the earlier draft. tm=$(mount_of /tmp); tmp_mp=${tm%% *}; tmp_ty=${tm#* } if [ "$tmp_mp" != "/" ]; then bst_ok iozone3/tmpfs-control else bst_notok iozone3/tmpfs-control \ "/tmp resolves to / ($tmp_ty), so the work-directory check above cannot distinguish the two" fi # Hand iozone a target that already exists with a usable mode -- see the header. # This, and the -w on the sweep below, are the two halves of one workaround: the # pre-create stops iozone's mode-0 O_CREAT ever happening, and -w stops it # unlinking and re-creating between sweep points, which would walk back into it. # # BRINTOS-WORKAROUND: hardwarejs#27 remove — iozone's sanity check creates its target with mode 0 and the virtio-fs backend then refuses to reopen it as the host account; pre-creating the file at 0644 and passing -w avoids that create. Both go when #27 lands, and iozone3/creates-own-target turns green on its own to say so. prep() { rm -f "$1"; : > "$1"; chmod 644 "$1"; } # --- the bounded automatic sweep --------------------------------------------- # Run once, into a file. Every table assertion below reads that one run rather # than re-invoking iozone, so the suite costs one sweep and the cases cannot # disagree with each other about which run they are describing. prep "$W/sweep.dat" iozone -a -n 64k -g 128k -y 4k -q 8k -w -f "$W/sweep.dat" > "$W/auto" 2>&1 arc=$? check_eq iozone3/auto-rc 0 "$arc" check_out iozone3/auto-complete "iozone test complete." cat "$W/auto" check_out iozone3/auto-mode "Auto Mode" cat "$W/auto" # --- identity, read off the sweep's own banner (no extra runs) --------------- check_out iozone3/version '$Revision: 3.506 $' cat "$W/auto" # The ILP32 witness, alongside mbw's "Long uses 4 bytes." iozone picks MODE off # __LP64__ (iozone.c:90), so THE SAME make target that prints "64 bit mode." on # the x86_64 build host must print this here. A wasm32 build claiming 64 bit # would mean the data model is not what the rest of the image assumes. check_out iozone3/ilp32 "Compiled for 32 bit mode." cat "$W/auto" # meta-oe's default EXTRA_OEMAKE selects upstream's generic `linux` target, which # is what -DNAME='"linux"' stamps into the banner. A build that had fallen onto # one of upstream's per-architecture targets would say so here. check_out iozone3/build-target "Build: linux" cat "$W/auto" # --- the table is complete and well-formed ----------------------------------- # The 13 operations iozone claims to measure, pinned by name AND order. This is # the ticket's "reports a full result table for every operation it claims to # measure", stated where it can be checked: the repeated names (read, write, # rewrite) are disambiguated by the qualifier row above them, asserted next. cols=$(awk '$1 == "kB" && $2 == "reclen" { s = ""; for (i = 3; i <= NF; i++) s = s $i " "; print s; exit }' "$W/auto") check_eq iozone3/table-columns \ "write rewrite read reread read write read rewrite read fwrite frewrite fread freread " "$cols" quals=$(awk '$1 == "random" && $2 == "random" { print $1" "$2" "$3" "$4" "$5; exit }' "$W/auto") check_eq iozone3/table-qualifiers "random random bkwd record stride" "$quals" # The four (file size, record size) points the bounds ask for, in order. This # subsumes a row count and says which point is missing when one is -- which is # how the platform defect first showed itself: the sweep completed point 1 and # died on point 2. pts=$(awk '$1 ~ /^[0-9]+$/ && $2 ~ /^[0-9]+$/ { printf "%s/%s ", $1, $2 }' "$W/auto") check_eq iozone3/auto-points "64/4 64/8 128/4 128/8 " "$pts" rows=$(awk '$1 ~ /^[0-9]+$/ && $2 ~ /^[0-9]+$/ { n++ } END { print n+0 }' "$W/auto") check_eq iozone3/auto-row-count 4 "$rows" # A row short of 15 fields is an operation that ran on some points and not # others -- the failure a "did the run complete" check cannot see, because # iozone still prints "iozone test complete." and still exits 0. short=$(awk '$1 ~ /^[0-9]+$/ && $2 ~ /^[0-9]+$/ && NF != 15 { printf "%s/%s(%d) ", $1, $2, NF }' "$W/auto") if [ -z "$short" ]; then bst_ok iozone3/every-op-reported else bst_notok iozone3/every-op-reported "$(_bst_1line "rows not carrying all 13 operations: $short")"; fi # Every cell strictly positive and finite. `inf` is the form a stopped clock # actually takes here (elapsed divides into the rate), so it is named explicitly # rather than left to the > 0 test, which inf passes. bad=$(awk '$1 ~ /^[0-9]+$/ && $2 ~ /^[0-9]+$/ { for (i = 3; i <= NF; i++) if ($i ~ /inf|nan/ || $i + 0 <= 0) printf "%s/%s:col%d=%s ", $1, $2, i, $i }' "$W/auto") if [ -z "$bad" ]; then bst_ok iozone3/all-cells-measured else bst_notok iozone3/all-cells-measured "$(_bst_1line "non-positive or non-finite cells: $bad")"; fi # --- the -i selectors pick operations, and each one costs a column pair ------ # Asserted by field count rather than by rate, because the point is which # operations RAN. -i 0 is write+rewrite, -i 1 adds read+reread, -i 2 adds random # read+write; a parser that ignored -i entirely would report 15 fields for all # three, and one that took only the last -i would report 4 for all three. nf() { awk '$1 ~ /^[0-9]+$/ && $2 ~ /^[0-9]+$/ { print NF; exit }' "$1"; } prep "$W/i0.dat"; iozone -s 64k -r 4k -i 0 -w -f "$W/i0.dat" > "$W/i0" 2>&1 prep "$W/i1.dat"; iozone -s 64k -r 4k -i 0 -i 1 -w -f "$W/i1.dat" > "$W/i1" 2>&1 prep "$W/i2.dat"; iozone -s 64k -r 4k -i 0 -i 1 -i 2 -w -f "$W/i2.dat" > "$W/i2" 2>&1 check_eq iozone3/select-write-only 4 "$(nf "$W/i0")" check_eq iozone3/select-write-read 6 "$(nf "$W/i1")" check_eq iozone3/select-write-read-rnd 8 "$(nf "$W/i2")" # --- the file iozone wrote is the file it was asked for ---------------------- # The only case here that checks the FILESYSTEM rather than the benchmark: -s 64k # must leave exactly 65536 bytes on the virtiofs root, and iozone must have # rewritten the empty file prep() handed it rather than reporting a rate for a # write that never landed. prep "$W/keep.dat" iozone -s 64k -r 4k -i 0 -w -f "$W/keep.dat" > "$W/keep" 2>&1 check_eq iozone3/written-file-size 65536 "$(wc -c < "$W/keep.dat" 2>/dev/null | tr -d ' ')" # --- the error paths, by exit status AND by message -------------------------- # Both halves, for the same reason the verdict tables elsewhere assert both: the # status is what a calling script branches on, the text is what tells a human # which of the two failures happened. iozone's codes are distinct per cause -- # 44 for a target it cannot create, 58 for a read of a file that is not there -- # so a build that had collapsed them would still pass a bare "non-zero" check. # Neither target is prep'd: their absence is the condition under test. iozone -s 64k -r 4k -i 0 -f "$W/nodir/x" > "$W/e44" 2>&1 e44=$? check_eq iozone3/unwritable-target-rc 44 "$e44" check_out iozone3/unwritable-target-msg "Can not open temp file:" cat "$W/e44" rm -f "$W/absent.dat" iozone -s 64k -r 4k -i 1 -f "$W/absent.dat" > "$W/e58" 2>&1 e58=$? check_eq iozone3/read-missing-file-rc 58 "$e58" check_out iozone3/read-missing-file-msg "Can not open temporary file" cat "$W/e58" # --- fileop: the metadata half ------------------------------------------------ # iozone times the data path; fileop times directory operations, which is the # only coverage on this image that puts a number on virtiofs metadata. -f 2 is # 2^3 = 8 files, small enough to finish in the same breath as the sweep. fileop # creates its own files with a sane mode, so it needs no prep and is unaffected # by the defect below. mkdir -p "$W/fo" fileop -f 2 -s 1k -d "$W/fo" > "$W/fop" 2>&1 frc=$? check_eq iozone3/fileop-rc 0 "$frc" fcols=$(awk '$1 == "." { s = ""; for (i = 2; i <= NF; i++) s = s $i " "; print s; exit }' "$W/fop") check_eq iozone3/fileop-columns \ "mkdir chdir rmdir create open read write close stat access chmod readdir link unlink delete Total_files " "$fcols" # The A (average) row: the force factor, 15 rates, then the file count. 2^3 = 8. check_eq iozone3/fileop-row-fields 18 "$(awk '$1 == "A" { print NF; exit }' "$W/fop")" check_eq iozone3/fileop-file-count 8 "$(awk '$1 == "A" { print $NF; exit }' "$W/fop")" fbad=$(awk '$1 == "A" { for (i = 3; i < NF; i++) if ($i ~ /inf|nan/ || $i + 0 <= 0) printf "col%d=%s ", i, $i }' "$W/fop") if [ -z "$fbad" ]; then bst_ok iozone3/fileop-all-ops-measured else bst_notok iozone3/fileop-all-ops-measured "$(_bst_1line "non-positive or non-finite metadata rates: $fbad")"; fi # --- EXPECTED REDS: guest root's DAC override does not reach the virtio-fs ----- # --- backend, so iozone cannot create its own target on the cloud rootfs ------- # Everything above works only because prep() hands iozone a file that already # exists. These two cases assert the behaviour WITHOUT that help, which is what # anyone typing `iozone -a -f ` on the virtiofs root actually gets. Both # are stated positively -- "root can write the file it owns", "iozone creates its # own target" -- so the day the backend honours the guest's credentials they go # green on their own and nobody has to remember to delete an exclusion. # # brintos/hardwarejs#27. # # The first case is the defect with iozone removed from it entirely: create a # file, chmod it to 0000, and write it as uid 0. Linux grants that through # CAP_DAC_OVERRIDE and the same three lines pass on the /tmp tmpfs in this very # guest; on the virtiofs root the backend re-checks the open as the host account # and returns EACCES. That contrast is what makes this a backend defect rather # than a kernel one. rm -f "$W/mode0"; : > "$W/mode0"; chmod 000 "$W/mode0" if ( echo x > "$W/mode0" ) 2>/dev/null; then bst_ok iozone3/root-writes-mode-0000 else bst_notok iozone3/root-writes-mode-0000 \ "EACCES writing a mode-0000 file as uid 0 on the virtiofs root (succeeds on the /tmp tmpfs in this same guest) -- brintos/hardwarejs#27" fi rm -f "$W/mode0" # The second is the consequence for this package: iozone's own sanity check # (iozone.c:7824) opens its target O_CREAT|O_WRONLY with mode 0 and then # ftruncates it. Given a path that does not yet exist, that creates the very # mode-0000 file the case above cannot reopen, and iozone exits 44 reporting # "Sanity check failed. Do not deploy this filesystem in a production # environment !" -- which is precisely the judgement that check exists to make. rm -f "$W/fresh.dat" iozone -s 64k -r 4k -i 0 -w -f "$W/fresh.dat" > "$W/fresh" 2>&1 frc2=$? if [ "$frc2" -eq 0 ]; then bst_ok iozone3/creates-own-target else bst_notok iozone3/creates-own-target \ "rc=$frc2 on a target iozone had to create itself; its sanity check leaves a mode-0000 file the backend will not reopen -- brintos/hardwarejs#27" fi rm -rf "$W"