Conversation
The TCP receive path read one whole block (the client's `-l` length) per poll event with a blocking `readinto()`, which on MicroPython returns only once the buffer is full. The sender is not required to end the stream on a block boundary, and the reference iperf3 client usually doesn't when its writes are larger than the socket can take at once: at the end of the test it abandons the block it is in the middle of writing and sends TEST_END. The server then waits forever for the rest of that block, never reads TEST_END from the control socket, and both ends hang. That makes `iperf3 -c <board>` with the default 128k block size stall on most runs, while `-l 500` works because such a small write is never split. Fix this by making the data socket non-blocking when it's used to receive TCP data, and counting however many bytes each read returns. The same applies to the client in reverse mode. Tested on an OPENMV_N6 over Ethernet with iperf 3.17.1 as the client and its default arguments: 2 of 12 runs completed before this change, 12 of 12 after. Also tested with iperf 3.9, 3.15, 3.16 and 3.22 as the client, with `-l 500` and `-R`, and with the board as the client (forward and reverse) against `iperf3 -s`. Signed-off-by: Kwabena W. Agyeman <kwagyeman@live.com>
The reference iperf3 client only sends "pacing_timer" in its parameters from version 3.2 onwards. With an older client the server raised `KeyError: pacing_timer` after the streams were created and the client hung. Use the same 1000ms default that this module's own client sends. Tested on an OPENMV_N6 over Ethernet with iperf 3.1.3 as the client. Signed-off-by: Kwabena W. Agyeman <kwagyeman@live.com>
The server accepted a single data connection whatever the client asked for, so `iperf3 -c <board> -P 2` never started: the client waited for its second stream to be accepted and the server waited for data on the first. Accept as many TCP data connections as the client's "parallel" parameter says, poll them all, and report the bytes transferred per stream in the results (the reference implementation numbers its streams 1, 3, 4, ...). The interval lines printed by the server remain the total of all streams. The client opens its streams back to back, so the listening socket is re-created with a backlog large enough to queue all of them before the client is asked to create them. The number of streams that work is limited by the TCP connections the port can have open at once. UDP and the client are unchanged, they still use one stream. Tested on an OPENMV_N6 over Ethernet with iperf 3.17.1 as the client: -P 2 and -P 4 sending to the board, -P 2 -R, single stream TCP both directions, and UDP both directions. Signed-off-by: Kwabena W. Agyeman <kwagyeman@live.com>
At the end of a reverse mode TCP test the server sent data until its socket buffer was full, then asked the client for its results and waited. A client that reads whole blocks with a blocking read (iperf 3.1.3 does) can be left waiting for the rest of a block that the server never sends, so it never sees the request and both ends hang. Ask for the results first, then keep sending on the data streams until the client responds on the control connection. Tested on an OPENMV_N6 over Ethernet with `-R` from iperf 3.1.3 (hung before, 3 of 3 runs complete now), 3.9, 3.15, 3.17.1 and 3.22, and with `-R -P 2` from 3.1.3 and 3.17.1. Signed-off-by: Kwabena W. Agyeman <kwagyeman@live.com>
Signed-off-by: Kwabena W. Agyeman <kwagyeman@live.com>
kwagyeman
force-pushed
the
kwabena/iperf3_partial_block
branch
from
October 2, 2026 16:40
9a505eb to
d58b242
Compare
Contributor
Author
|
@dpgeorge - ipref is much better now. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes to the
iperf3server so it works with the reference iperf3 client's default arguments, with parallel streams, and with old clients. Found by running iperf 3.1.3 to 3.22 against an OPENMV_N6 over Ethernet.Don't wait for a full block when receiving TCP data.
iperf3 -c <board>with default arguments (128k blocks) hangs on most runs. The TCP receive path reads one whole block per poll event with a blockingreadinto(), which on MicroPython returns only once the buffer is full. The sender is not required to end the stream on a block boundary, and the reference client usually doesn't when a block is larger than the socket takes in one write: at the end of the test it abandons the block it is in the middle of writing and sendsTEST_END. The server then waits forever for the rest of that block and never readsTEST_ENDfrom the control socket.Measured during a hang:
ss -tinon the data connection: bytes acked = 202 × 131072 + 22456, so the stream ended mid-block (the client's own total only counts whole blocks)test->state == TEST_END, so it has sentTEST_ENDand is waiting forEXCHANGE_RESULTSrecvninto()on the data socketThe data socket is now non-blocking when it is used to receive TCP data, and each read counts however many bytes it returns. The same applies to the client in reverse mode.
Accept clients that do not send a pacing timer. The reference client only sends
pacing_timerfrom version 3.2 onwards. With an older client the server raisedKeyError: pacing_timerand the client hung. It now defaults to 1000 ms, the value this module's own client sends.Support parallel TCP streams in the server. The server accepted one data connection whatever the client asked for, so
-P 2never started. It now accepts as many TCP data connections as the client'sparallelparameter says, polls them all, and reports bytes per stream in the results (the reference implementation numbers its streams 1, 3, 4, ...). The interval lines printed by the server remain the total of all streams. The client opens its streams back to back, so the listening socket is re-created with a backlog large enough to queue all of them before the client is asked to create them.Keep sending in reverse mode until the client responds. At the end of a reverse mode TCP test the server sent until its socket buffer was full, then asked for the client's results and waited. A client that reads whole blocks with a blocking read (iperf 3.1.3) is left waiting for the rest of a block and never sees the request. The server now asks for the results first and keeps sending until the client responds.
Testing
OPENMV_N6 over Ethernet, iperf3 client on a Linux host, default arguments unless noted. Runs completed, before and after the first commit:
-l 500Throughput with
-l 500is unchanged (42.8 Mbit/s before, 44.8 after, medians of 5).With all commits, iperf 3.17.1 as the client unless noted:
-R-P 2-P 4,-P 4 -l 500-P 2 -R-u -b 20M,-u -R -b 20M-R-l 500,-P 2,-P 2 -R-RAlso ran the board as the client against
iperf3 -s, forward and reverse, 3 runs each, all completed (first commit only).Not working on this board, and not addressed here:
-P 5: it needs 6 TCP connections and the port allows 5.-P 3 -R: throughput collapses andsend()raisesEIO.Trade-offs and Alternatives
Reads are now as large as whatever has arrived rather than one full block, so there are more Python-level reads per second with large blocks. Throughput did not drop in the runs above.
Re-creating the listening socket after the parameter exchange is the only way to size the backlog exactly, because the stream count isn't known at the first
listen(). A fixed larger backlog would also work but would be a guess.UDP and the client side still use a single stream.