. "$BST_LIB" # BST-TIMEOUT: 600 # # tiobench 0.3.3 (meta-oe recipes-benchmark) -- the "threaded I/O tester": # /usr/bin/tiotest drives four phases (sequential write, random write, sequential # read, random read) against real files with N pthreads each, and the perl front # ends /usr/bin/tiobench.pl and /usr/bin/tiosum.pl run it repeatedly and report. # # It is the second STORAGE benchmark on this image after iozone3, and the two ask # different questions of the same virtiofs cloud rootfs: iozone sweeps one thread # over many record and file sizes, tiobench holds the size fixed and adds # concurrency. Everything before it -- dhrystone, whetstone, linpack, nbench, # libc-bench, mbw -- stays inside the process. # # FIVE FACTS SHAPE THIS FILE. # # 0. THE WORK DIRECTORY IS THE SUBJECT, AND /tmp IS THE WRONG ONE. The kernel # mounts a tmpfs over /tmp for session-local scratch, so `mktemp -d` lands # off the filesystem under test: a run there measures RAM, never touches the # cloud filesystem, and still prints a full healthy table at plausible rates. # 84-iozone3 measured the gap at ~60x. 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 (the negative control -- without it, "the work directory # is on /" could be true by construction and prove nothing), and the suite # refuses to measure anything if no such directory can be had. # 1. getrusage(2) IS UNWIRED ON THIS ARM (brintos/linux#78, filed from # BRINTOS-935: no [98] row in the wasm32 syscall table, so it returns # -ENOSYS), and tiotest is one of the few programs that CHECKS it -- # timer_start() calls perror + exit(11) on the failure, before a single block # is written. tiobench_%.bbappend's 0001 zeroes the rusage struct instead of # exiting, because every headline figure tiotest reports (the per-phase rate # and the average/maximum latency) is differenced from gettimeofday(), which # works here; only the two CPU columns come from getrusage. So those columns # read 0.0 % on this image -- the honest answer when no counters were # obtained -- and cpu-accounting-available below is the anchor case that says # so in the tool's own output. It is stated POSITIVELY: it goes green by # itself the day #78 lands, and nothing here needs editing then. # 2. tiotest IS THREADED BY CONSTRUCTION and pthreads are not whole here. # do_test() creates numThreads pthreads per phase and joins them all, so no # phase runs unless thread creation does. Measured on this arm: # pthread_create FAILS OUTRIGHT with EAGAIN ("Error creating threads: # Resource temporarily unavailable", rc=255) -- at 1, 2 and 4 threads alike, # against 964 MB free, threads-max 16362 and no ulimit set, so it is the # platform's answer to thread creation and not a resource ceiling # (linux-in-the-browser#85; brintos/hardwarejs#12 is the join-side futex the # other witnesses trap on). That is a DIFFERENT shape from every prior # witness on #85, which is why the cases below assert the absence of success # rather than matching a signature string -- see the note above the anchor # case. A thread count of 2 rather than 1 is deliberate -- concurrency is the # whole subject of this benchmark, and a single-threaded run would report a # number while measuring none of it. # 3. THE FILES ARE SMALL ON PURPOSE. 1 MB per thread, 4 kB blocks, 100 random # ops. Guest memory is not returned to the kernel on process exit # (brintos/linux#25), so a suite given the hundreds of MB tiobench is normally # pointed at would starve everything scheduled behind it in this boot -- and # the virtiofs root is slow enough (84-iozone3 measured 727 kB/s writing) that # it would also cost minutes. # 4. THE POLLING IS BUILTIN-ONLY. `read < /proc/uptime` is a builtin; `cut` and # `date` are execs, and an exec-per-iteration poll grows the host guest # process by gigabytes (brintos/hwjs-cc#69 leaks a park slot per exec). # `sleep N` does not sleep (brintos/linux#48) and timeout(1) does not fire # (alarm(2) is ENOSYS, brintos/linux#77), so each run is bounded by this file # killing it. require tiotest tiobench/present VER=0.3.3 FSIZE=1 # MB per thread BLK=4096 THREADS=2 RANDOPS=100 now() { read NOW _rest < /proc/uptime; NOW=${NOW%.*}; } spin() { _i=0; while [ $_i -lt 20000 ]; do _i=$((_i + 1)); done; } # run_bounded -- run with a wall-clock bound, since # nothing on this image can impose one. Sets RB_RC to the command's status, or # 124 iff WE killed it. A done-sentinel FILE is what is polled: a finished # background child is a ZOMBIE until reaped and `kill -0` SUCCEEDS on one, so a # kill-0 poll would report every case as timed-out even when it completed. run_bounded() { _lim=$1; _tag=$2; shift 2 rm -f "$RD/$_tag.done" { "$@" > "$RD/$_tag.log" 2>&1; echo $? > "$RD/$_tag.done"; } & _p=$! now; _s=$NOW while [ ! -f "$RD/$_tag.done" ]; do now if [ $((NOW - _s)) -ge "$_lim" ]; then kill -9 "$_p" 2>/dev/null RB_RC=124 return 0 fi spin done read RB_RC < "$RD/$_tag.done" return 0 } # 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 } # column -- one cell of one of tiotest's two # tables.
is 1 for the results table and 2 for the latency table, which # is what disambiguates them: both carry a row whose Item is "Write", and the # only difference is the " N MBs" suffix the results table appends. The latency # header line is the boundary between them. column() { awk -F'|' -v want="$2" -v col="$3" -v tbl="$1" ' /Average latency/ { lat = 1; next } NF > 4 { item = $2 gsub(/^[ \t]+|[ \t]+$/, "", item) sub(/[ \t]+[0-9]+ MBs$/, "", item) if (item != want) next if ((lat ? 2 : 1) != tbl) next v = $(col) gsub(/^[ \t]+|[ \t]+$/, "", v) sub(/[ \t]*(MB\/s|ms|s|%)$/, "", v) gsub(/[ \t]+$/, "", v) print v; exit }' "$4" } # positive -- the figure exists and is greater than zero. # The regex also rejects "inf" and "nan": tiotest divides its elapsed time into # the rate, so a stopped clock would print inf rather than 0 and the table would # still look like a measurement. positive() { if awk -v v="$2" 'BEGIN { exit !(v ~ /^[0-9]+(\.[0-9]+)?$/ && v + 0 > 0) }'; then bst_ok "$1" else bst_notok "$1" "$3 parsed as '$2', which is not a positive number" fi } # --- packaging --------------------------------------------------------------- # tiotest plus BOTH perl front ends: the reporting wrapper is half of what this # package is for, and it is the half that would silently go missing if the perl # RDEPENDS ever stopped being satisfied. miss="" for f in /usr/bin/tiotest /usr/bin/tiobench.pl /usr/bin/tiosum.pl; do [ -x "$f" ] || miss="$miss $f" done check_eq tiobench/inventory "" "${miss# }" # busybox has no tiotest applet, so a bare `tiotest` can only be this port. check_eq tiobench/resolves-to-port "/usr/bin/tiotest" "$(command -v tiotest)" # --- identity, no run involved ----------------------------------------------- # -h exits 1, so this cannot be check_out; the banner carries the version the # Makefile was built from, so a PV bump surfaces here rather than as a puzzling # output change downstream. hout=$(tiotest -h 2>&1) case "$hout" in *"tiotest v$VER"*) bst_ok tiobench/version-banner ;; *) bst_notok tiobench/version-banner "no 'tiotest v$VER' in the help banner: $(_bst_1line "$hout")" ;; esac # --- fact 0: what is being measured, and the control that proves it ---------- rty=$(awk '$2 == "/" { print $3; exit }' /proc/mounts) check_eq tiobench/rootfs-is-virtiofs virtiofs "$rty" W= for d in /home/root /var/lib /usr/lib/brintos-tests; do [ -d "$d" ] || continue c="$d/.bst-tiobench.$$" 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 tiobench/workdir-on-virtiofs-root else bst_notok tiobench/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. tm=$(mount_of /tmp); tmp_mp=${tm%% *}; tmp_ty=${tm#* } if [ "$tmp_mp" != "/" ]; then bst_ok tiobench/tmpfs-control else bst_notok tiobench/tmpfs-control \ "/tmp resolves to / ($tmp_ty), so the work-directory check above cannot distinguish the two" fi # Run logs live on the tmpfs deliberately: they are the suite's own bookkeeping, # not part of what is being measured, and writing them beside the test files # would put them in the tree the cleanup case inspects. RD=/tmp/.bst-tiobench rm -rf "$RD"; mkdir -p "$RD" # --- the run ----------------------------------------------------------------- run_bounded 420 tiotest tiotest -d "$W" -f "$FSIZE" -b "$BLK" -t "$THREADS" -r "$RANDOPS" sed 's/^/TIOTEST /' "$RD/tiotest.log" 2>/dev/null LOG=$RD/tiotest.log # THE ANCHOR CASE for fact 2. It asserts the ABSENCE OF SUCCESS -- a results # table was produced -- and carries whatever tiotest said in the red's detail, # rather than matching on any failure signature. # # That shape is deliberate and is the practice brintos/linux-in-the-browser#85 # itself asks for: programs hit that gap in genuinely different ways at one and # the same pin (a trapped park handle, glibc's futex_fatal_error via # brintos/hardwarejs#12, and -- measured here -- a plain EAGAIN out of # pthread_create), so a case keyed on one signature reads a second failure mode # as a fix. Asserting the positive fact instead means this case goes green the # day the threads run and cannot go green for any other reason. # # MEASURED 2026-08-17 on this image: `tiotest -t 1`, `-t 2` and `-t 4` all fail # identically with "Error creating threads: Resource temporarily unavailable" # (EAGAIN) against 964 MB free, threads-max 16362 and no ulimit in the way -- so # ONE thread is one too many, and it is not a resource limit. Note this port # fails GRACEFULLY where earlier witnesses on #85 trap: tiotest checks # pthread_create's return and exits, so it never reaches the park protocol, the # guest survives, and this suite can therefore drive the real thing rather than # assert it blind the way 79-fftw must. THR=0 if [ -z "$(column 1 Write 4 "$LOG")" ]; then THR=1 bst_notok tiobench/threads-usable \ "brintos/linux-in-the-browser#85: tiotest creates numThreads pthreads per phase and joins them, and no phase produced a results-table row on this arm, so nothing was measured -- tiotest said: $(_bst_1line "$(tail -3 "$LOG" 2>&1)")" else bst_ok tiobench/threads-usable fi # The other anchor, for fact 1 -- and it is NOT observable yet, which the case # says in its own words rather than mis-attributing. getrusage fills the two CPU # columns; 0001 is what stops a failing getrusage being exit(11), but no table is # produced at all while the run dies at fact 2, so #78's effect cannot be read # here. When threads land, this case reads the columns: 0.0/0.0 means #78 is # still unwired, a real percentage means it landed. Both anchors then flip green # independently, which is why they are separate cases. cpu_w=$(column 1 Write 5 "$LOG") cpu_s=$(column 1 Write 6 "$LOG") if [ -z "$cpu_w" ]; then if [ "$THR" -eq 1 ]; then bst_notok tiobench/cpu-accounting-available \ "downstream of tiobench/threads-usable -- no phase ran, so there is no results table whose CPU columns could show whether brintos/linux#78 (getrusage unwired) still bites; 0001 rests on the x86_64 A/B in the bbappend until then" else bst_notok tiobench/cpu-accounting-available \ "no results table to read the CPU columns from, and no thread complaint to explain it: $(_bst_1line "$(tail -3 "$LOG" 2>&1)")" fi elif awk -v u="$cpu_w" -v s="$cpu_s" 'BEGIN { exit !(u + 0 == 0 && s + 0 == 0) }'; then bst_notok tiobench/cpu-accounting-available \ "brintos/linux#78: getrusage(2) is unwired on wasm32, so tiotest reports Usr $cpu_w % / Sys $cpu_s % for a run that did real work; tiobench_%.bbappend 0001 is what keeps that from being exit(11) instead" else bst_ok tiobench/cpu-accounting-available fi # `downstream ` -- a case that cannot be established because the # run did not produce a table. Reported `not ok`, never `skip`: the tool is # PRESENT here, and a skip would make the suite green over a benchmark that # produced no number at all. downstream() { bst_notok "$1" "downstream of tiobench/run-completes -- $2"; } if [ "$RB_RC" -eq 0 ]; then bst_ok tiobench/run-completes elif [ "$RB_RC" -eq 124 ]; then bst_notok tiobench/run-completes "killed at 420 s by this suite: $(_bst_1line "$(tail -3 "$LOG" 2>&1)")" else bst_notok tiobench/run-completes "rc=$RB_RC $(_bst_1line "$(tail -3 "$LOG" 2>&1)")" fi # --- the four phases: a rate and a latency each ------------------------------ # Both tables are read from the ONE run above rather than re-invoking tiotest per # phase, so the cases cannot disagree about which run they are describing. for pair in "write:Write" "random-write:Random Write" "read:Read" "random-read:Random Read"; do slug=${pair%%:*}; item=${pair#*:} if [ "$RB_RC" -ne 0 ]; then downstream "tiobench/rate-$slug" "no results table" downstream "tiobench/latency-$slug" "no latency table" continue fi positive "tiobench/rate-$slug" "$(column 1 "$item" 4 "$LOG")" "the $item rate (MB/s)" positive "tiobench/latency-$slug" "$(column 2 "$item" 3 "$LOG")" "the $item average latency (ms)" done # --- cleanup: tiotest unlinks every file it wrote ---------------------------- # NOT asserted as a pass when the run failed. An empty work directory after a run # that never wrote anything means nothing was measured, and reporting that green # is the one failure shape that looks like success. if [ "$RB_RC" -ne 0 ]; then downstream tiobench/cleanup-removes-files \ "nothing was written, so cleanup is unmeasured (an empty tree here is not evidence)" else left=$(find "$W" -type f 2>/dev/null | wc -l) if [ "$left" -eq 0 ]; then bst_ok tiobench/cleanup-removes-files else bst_notok tiobench/cleanup-removes-files \ "$left file(s) still under $W after the run: $(_bst_1line "$(find "$W" -type f 2>/dev/null | head -3)")" fi fi # --- the perl front end ------------------------------------------------------ # The identifier is pinned rather than left to default to `uname -r`, so the data # row under each section heading is identifiable without knowing this guest's # kernel version string. ID=bst run_bounded 420 tiobench tiobench.pl --dir "$W" --identifier "$ID" \ --size "$((FSIZE * THREADS))" --numruns 1 --block "$BLK" \ --random "$RANDOPS" --threads "$THREADS" sed 's/^/TIOBENCH.PL /' "$RD/tiobench.log" 2>/dev/null PLOG=$RD/tiobench.log if [ "$RB_RC" -eq 0 ]; then bst_ok tiobench/pl-run-completes elif [ "$RB_RC" -eq 124 ]; then bst_notok tiobench/pl-run-completes "killed at 420 s by this suite: $(_bst_1line "$(tail -3 "$PLOG" 2>&1)")" else bst_notok tiobench/pl-run-completes "rc=$RB_RC $(_bst_1line "$(tail -3 "$PLOG" 2>&1)")" fi # One section per phase, each followed by a data row -- which is the whole of # what the front end adds over tiotest. Checked structurally (the row is the # pinned identifier and at least eight further columns) rather than against any # figure: the rate column is a fixed-width %6.2f that legitimately prints ###### # when the number does not fit, so asserting a parseable rate here would be red # on a fast host and is tiotest's job anyway. for sect in "Sequential Reads" "Random Reads" "Sequential Writes" "Random Writes"; do slug=$(printf '%s' "$sect" | tr 'A-Z ' 'a-z-') if [ "$RB_RC" -ne 0 ]; then bst_notok "tiobench/pl-row-$slug" "downstream of tiobench/pl-run-completes -- no report" continue fi # The front end runs tiotest itself, so a thread gap upstream leaves it with # nothing to tabulate. Attributed rather than reported as a bare absence: the # heading IS printed and the report DOES complete (that is what 0002 buys), so # the only thing missing is the measurement fact 2 prevented. if [ "$THR" -eq 1 ]; then bst_notok "tiobench/pl-row-$slug" \ "downstream of tiobench/threads-usable -- tiobench.pl runs tiotest itself, so it printed the '$sect' heading with no data behind it" continue fi row=$(awk -v s="$sect" -v id="$ID" ' $0 == s { want = 1; next } want && $1 == id && NF >= 9 { print "hit"; exit } want && NF > 0 { want = 0 }' "$PLOG") if [ "$row" = "hit" ]; then bst_ok "tiobench/pl-row-$slug" else bst_notok "tiobench/pl-row-$slug" \ "no '$ID' data row of 9+ columns under the '$sect' heading" fi done rm -rf "$RD" "$W"