. "$BST_LIB" # BST-TIMEOUT: 420 # # Measured, not guessed: 68 s for this file on a fresh scoped guest # (2026-08-17), so 420 is ~6x that. It is declared because the runner default # is 60 s and one case here is a HANG bounded by this file at 25 s -- a suite # killed at its bound reports one red instead of every result below. # # qperf 0.4.11 (meta-oe recipes-benchmark) -- one binary that is both ends of a # socket measurement: a bare `qperf` is the server, `qperf ` the # client. Both run in this guest over loopback, which is the only interface it # has (/proc/net/dev lists lo and nothing else). qperf's RDMA half configures # out here -- there is no libibverbs in the sysroot and the recipe's configure # answers `no` to all three fabric checks -- so what is under test is the # socket half: conf, tcp_bw, tcp_lat, udp_lat. # # FOUR FACTS SHAPE THIS FILE, and three of them are why qperf_%.bbappend # carries three patches. # # 1. STOCK QPERF CANNOT SERVE HERE AT ALL. Both of its passive getaddrinfo # lookups ask for AF_INET6 outright, and this guest has no IPv6 (no # /proc/net/if_inet6, no /proc/net/tcp6), so the control socket reports # "unable to bind to listen port" and the per-test data socket reports # "unable to make TCP socket". Fixed in the bbappend as a qperf bug # (BRINTOS-UPSTREAM-FIX) -- the client half already asked for AF_UNSPEC. # server-listens below is the case that notices if that ever regresses. # 2. NO TEST CAN BE DRIVEN BY TIME. qperf's duration mode arms # setitimer(ITIMER_REAL) and waits for SIGALRM. This arm never delivers it # (brintos/linux#21, with #77 for the alarm(2) half, and #79 for the trap # below), and a timed run ends # one of two ways here, both observed. Usually it TRAPS: qperf registers its # handlers with sigaction + SA_SIGINFO, and dispatching a signal into one of # those dies on "null function or function signature mismatch" inside # __wasm32_signal_dispatch_restart -- this arm cannot call a three-argument # handler -- so the process takes SIGILL where nuttcp merely hung # (BRINTOS-935). Sometimes nothing arrives at all and it hangs like nuttcp. Every test below is therefore count-driven, via --no_msgs, # which upstream documents as the general "instead of time" bound but # implements only for RDMA -- so it hangs on stock qperf everywhere, not # just here. timed-run-completes keeps one witness for the platform half. # 3. times(2) DOES NOT ADVANCE on this arm (brintos/linux#47), and every qperf # figure is computed # from it, so an unpatched port reports "latency = 0 ns" and no bandwidth # line at all for transfers whose message counts come back exactly right. # gettimeofday(2) does work here, and is what the third patch uses. That # is why *-latency-positive can be asserted rather than skipped. # 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). No # command substitution appears in any loop here. `sleep N` does not sleep # (brintos/linux#48) and timeout(1) does not fire (#77), so the one hanging # case is bounded by this file killing it. require qperf qperf/present VER=0.4.11 D=/tmp/.bst-qperf rm -rf "$D"; mkdir -p "$D" now() { read NOW _rest < /proc/uptime; NOW=${NOW%.*}; } spin() { _i=0; while [ $_i -lt 20000 ]; do _i=$((_i + 1)); done; } # listening -- true iff /proc/net/tcp carries a LISTEN (state 0A) row # for that local port. Read from /proc rather than through `ss` so the case # asserts the KERNEL's view and cannot fail for iproute2's reasons. listening() { awk -v p=":$1" '$4 == "0A" && index($2, p) { hit = 1 } END { exit !hit }' \ /proc/net/tcp 2>/dev/null } wait_listen() { now; _s=$NOW while :; do listening "$1" && return 0 now; [ $((NOW - _s)) -ge "$2" ] && return 1 spin done } # run_bounded -- run with a wall-clock bound, since # nothing on this image can impose one (fact 4). 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 reports every case as timed-out even when it completed. run_bounded() { _lim=$1; _tag=$2; shift 2 rm -f "$D/$_tag.done" { "$@" > "$D/$_tag.log" 2>&1; echo $? > "$D/$_tag.done"; } & _p=$! now; _s=$NOW while [ ! -f "$D/$_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 < "$D/$_tag.done" return 0 } # serve -- one server, one test, torn down after. # A server per case is deliberate: a client that dies on SIGILL (fact 2) leaves # the server's per-connection state mid-exchange, and a case must not be able to # fail because the case before it crashed. start_server() { qperf -lp "$1" > "$D/$3.srv" 2>&1 & SRV=$! wait_listen "$2" 30 } stop_server() { kill -9 "$SRV" 2>/dev/null; } # --- packaging --------------------------------------------------------------- check_eq qperf/inventory "" "$([ -x /usr/bin/qperf ] || echo /usr/bin/qperf)" # busybox has no qperf applet, so a bare `qperf` can only be this port. check_eq qperf/resolves-to-port "/usr/bin/qperf" "$(command -v qperf)" # --- argument surface, no sockets involved ----------------------------------- # qperf has no --version; its help is the only identity it prints without a # peer, and its version is asserted through conf below instead. check_out qperf/help-synopsis "Synopsis" sh -c 'qperf --help 2>&1; exit 0' # `qperf --help` alone prints only the overview -- the test list is behind # `--help tests`, which is what names the four socket tests this file drives. check_out qperf/help-lists-tcp-bw "tcp_bw" sh -c 'qperf --help tests 2>&1; exit 0' check_rc qperf/help-rc 0 sh -c 'qperf --help >/dev/null 2>&1' # --- the instrument is checked before it is used ----------------------------- # qperf ships no init script and no systemd unit, so nothing should be holding # its port. With something already LISTENing, server-listens would pass against # a server this suite did not start while the one it did start failed to bind. if listening 4D3D; then bst_notok qperf/port-free-before-start \ "something is already LISTENing on 19773 -- the assertions below would not be about this suite's server" else bst_ok qperf/port-free-before-start fi # --- the server binds -------------------------------------------------------- # THE case for the AF_INET6 patch. Stock qperf fails this on any IPv6-less # kernel, and every case below it is unreachable when it does. if start_server 19773 4D3D listen; then bst_ok qperf/server-listens else bst_notok qperf/server-listens \ "no LISTEN row for 19773 in /proc/net/tcp: $(_bst_1line "$(cat "$D/listen.srv" 2>&1)")" fi # --- conf: the one test that needs no clock, no data socket and no bound ------ run_bounded 60 conf qperf -lp 19773 127.0.0.1 conf sed 's/^/QPERF-conf /' "$D/conf.log" 2>/dev/null if [ "$RB_RC" -eq 0 ]; then bst_ok qperf/conf-completes else bst_notok qperf/conf-completes "rc=$RB_RC $(_bst_1line "$(cat "$D/conf.log" 2>&1)")"; fi # Both ENDS report the version, and it is the version the recipe pinned -- so a # PV bump surfaces here rather than as a puzzling output change downstream. The # two are asserted separately: a conf that reports the local end and not the # remote one means the exchange half-failed, which is a different defect from a # version mismatch. conf_of() { awk -v k="$1" '$1 == k { print $3 }' "$D/conf.log"; } check_eq qperf/conf-loc-version "$VER" "$(conf_of loc_qperf)" check_eq qperf/conf-rem-version "$VER" "$(conf_of rem_qperf)" check_out qperf/conf-reports-both-os "rem_os" cat "$D/conf.log" stop_server # --- the count-driven socket tests ------------------------------------------- # value_of -- the measured figure and its unit from a result line like # latency = 6.6 ms # bw = 1.2 MB/sec # Found by the '=' rather than by a fixed column, because the label widths # differ between tests and qperf pads them to its own alignment. value_of() { awk '/=/ { for (i = 1; i <= NF; i++) if ($i == "=") { print $(i+1); exit } }' "$1"; } # positive -- the figure exists and is greater than zero. This is # the assertion times(2) defeats: an unpatched port reaches it with the right # message counts and a value of exactly 0 (fact 3). positive() { _v=$(value_of "$2") if awk -v v="$_v" 'BEGIN { exit !(v ~ /^[0-9]+(\.[0-9]+)?$/ && v + 0 > 0) }'; then bst_ok "$1" else bst_notok "$1" "parsed '$_v' from: $(_bst_1line "$(cat "$2" 2>&1)")" fi } # latency -- one bounded count-driven latency run, # asserted twice, because the two fail independently: the run completed, and it # printed a positive figure. A qperf that exits 0 having printed "0 ns" is a # different defect from one that never returns. # ONE RETRY, deliberately, and only here. The result exchange these two depend # on stalls in the platform, not in qperf (see tcp_bw below): the server's reply # is left in its own socket's tx_queue and never delivered. For a bulk test that # is every run; for these two it is occasional, and a case that is red one run in # several is worth less than no case at all -- it trains a reviewer to ignore it. # So each gets a second attempt against a fresh server, and the failure message # says both attempts failed, so a real regression still reads as one. latency() { _case=$1; _p=$2; _h=$3; _t=$4 _try=0; _why="" while [ $_try -lt 2 ]; do _try=$((_try + 1)) if start_server "$((_p + _try - 1))" "$(printf %04X $((_p + _try - 1)))" "$_case"; then run_bounded 90 "$_case" qperf -lp "$((_p + _try - 1))" -n 100 127.0.0.1 "$_t" stop_server [ "$RB_RC" -eq 0 ] && break _why="$_why[try $_try] rc=$RB_RC $(_bst_1line "$(cat "$D/$_case.log" 2>&1)") " else stop_server RB_RC=-1 _why="$_why[try $_try] server never listened on $((_p + _try - 1)) " fi done sed "s/^/QPERF-$_case /" "$D/$_case.log" 2>/dev/null if [ "$RB_RC" -eq 0 ]; then bst_ok "qperf/$_case-completes" else bst_notok "qperf/$_case-completes" "$_why"; fi positive "qperf/$_case-latency-positive" "$D/$_case.log" } latency tcp-lat 19781 4D45 tcp_lat latency udp-lat 19783 4D47 udp_lat # --- tcp_bw: EXPECTED RED, and it is not qperf's fault ------------------------ # The transfer itself works -- both ends move all 10 messages and the server # logs that it sent its results -- and the client's wait for those results then # times out, at any timeout value. /proc/net/tcp says why, and it is not a # select() problem: while the client waits, the SERVER's end of the control # connection holds tx_queue=0x11B (283 bytes, one STAT) and the CLIENT's end # holds rx_queue=0, for as long as the client waits. The bytes are queued in the # sender and are never delivered over loopback. # # The trigger is bulk volume on the OTHER socket, not this test's shape: # tcp_bw -n2 (128 KB of data) completes, tcp_bw -n10 (640 KB) and above stall, # and tcp_lat -n2000 -- which is a ping-pong of 2000 one-byte messages, i.e. no # bulk at all -- completes fine. So once a few hundred KB have crossed the data # connection, a small write on the control connection, idle throughout that # transfer, stops being delivered. Filed as brintos/linux#80. # # Stated positively, so this case goes green by itself when that lands. if start_server 19776 4D40 tcp-bw; then run_bounded 40 tcp-bw qperf -lp 19776 -n 10 127.0.0.1 tcp_bw sed 's/^/QPERF-tcp-bw /' "$D/tcp-bw.log" 2>/dev/null if [ "$RB_RC" -eq 0 ]; then bst_ok qperf/tcp-bw-completes positive qperf/tcp-bw-rate-positive "$D/tcp-bw.log" else bst_notok qperf/tcp-bw-completes \ "rc=$RB_RC -- brintos/linux#80: the transfer completes on both ends and the server's 283-byte result then sits in its own tx_queue undelivered: $(_bst_1line "$(cat "$D/tcp-bw.log" 2>&1)")" bst_notok qperf/tcp-bw-rate-positive "no result line: tcp-bw-completes failed first" fi else bst_notok qperf/tcp-bw-completes "server never listened on 19776" bst_notok qperf/tcp-bw-rate-positive "server never listened on 19776" fi stop_server # --- the timed-mode witness, last and on its own port ------------------------ # EXPECTED RED. -t arms setitimer(ITIMER_REAL) and waits for SIGALRM. # The signal IS dispatched here -- unlike nuttcp, which hangs (BRINTOS-935) -- # and the dispatch TRAPS, because qperf registers its handlers with sigaction + # SA_SIGINFO and this arm's __wasm32_signal_dispatch_restart cannot call a # three-argument handler (brintos/linux#79). The process dies with SIGILL, rc 132. # # That is a sharper statement than "SIGALRM is not delivered": delivery is # attempted and the handler shape is what fails. Stated positively so this case # goes green by itself when signal dispatch learns SA_SIGINFO. # It runs last because a crashed client leaves its server mid-exchange. if start_server 19777 4D41 timed; then run_bounded 25 timed qperf -lp 19777 -t 2 127.0.0.1 tcp_bw sed 's/^/QPERF-timed /' "$D/timed.log" 2>/dev/null if [ "$RB_RC" -eq 0 ]; then bst_ok qperf/timed-run-completes else bst_notok qperf/timed-run-completes \ "rc=$RB_RC: a timed run never finishes here. Both endings have been seen -- 132, SIGILL, when the SIGALRM is dispatched into qperf's SA_SIGINFO handler and this arm's __wasm32_signal_dispatch_restart cannot call a three-argument handler (brintos/linux#79); and 124, this file killing it at 25 s, when the signal simply never arrives (brintos/linux#21, and #77 for the alarm(2) half)" fi else bst_notok qperf/timed-run-completes "server never listened on 19777" fi # --- stopping the server ----------------------------------------------------- # qperf has no shutdown command, so stopping it is a signal, and # brintos/linux#21 says signal delivery does not work at all on this arm. # MEASURED, IT PASSES, and that is worth its own case: it is evidence against # that issue's title, and it does NOT contradict the witness above -- an # explicit SIGTERM to a default disposition and a kernel-posted SIGALRM into a # SA_SIGINFO handler are different paths, and this file measures both. kill "$SRV" 2>/dev/null now; _s=$NOW while listening 4D41; do now; [ $((NOW - _s)) -ge 15 ] && break spin done if listening 4D41; then bst_notok qperf/server-stops-on-signal \ "kill $SRV returned 0 and qperf is still LISTENing on 19777 15 s later (brintos/linux#21 shape?)" else bst_ok qperf/server-stops-on-signal fi rm -rf "$D"