Skip to content

iperf3: Don't wait for a full block when receiving TCP data. - #1167

Merged
dpgeorge merged 5 commits into
micropython:masterfrom
kwagyeman:kwabena/iperf3_partial_block
Oct 28, 2026
Merged

dpgeorge merged 5 commits into
micropython:masterfrom
kwagyeman:kwabena/iperf3_partial_block

Conversation

@kwagyeman

@kwagyeman kwagyeman commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes to the iperf3 server 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.

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 blocking readinto(), which on MicroPython returns only once the buffer is full. The reference client usually ends the stream part-way through a block and then sends TEST_END, so the server waits forever for the rest of the block and never reads TEST_END. The data socket is now non-blocking when receiving 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_timer from 3.2 onwards; with an older client the server raised KeyError. It now defaults to 1000 ms.

Support parallel TCP streams in the server. The server accepted one data connection whatever the client asked for, so -P 2 never started. It now accepts as many as the client's parallel parameter says, polls them all, and reports bytes per stream in the results (the reference implementation numbers its streams 1, 3, 4, ...). The listening socket has a backlog of 4 so the client's streams can queue; that's also as many streams as lwIP's default of 5 TCP pcbs leaves room for.

Close the data streams when the test ends. The reference server closes its streams on TEST_END, before asking for results. This one kept them open, so with parallel streams their unread data could fill lwIP's receive buffers and the results exchange couldn't get through. Closing them also lets a client that reads whole blocks (iperf 3.1.3) in reverse mode see the end of the stream, which replaces the server's old "keep writing" loop.

Testing

OPENMV_N6 on MicroPython master, Ethernet and WiFi, iperf3 client on a Linux host.

test Ethernet WiFi
-l 5000 -P 2, before this revision 9/10 0/10
-l 5000 -P 2 10/10 10/10, and 200/200 with -t 3
default, -R, -l 500 13/13 13/13
-P 2 -R, -P 4, -P 4 -l 500 8/8 8/8
UDP -b 20M, both directions 4/4 3/4 (one timeout; 6/6 on rerun, same as the original server)
iperf 3.1.3: default, -R, -P 2, -P 2 -R, -l 500 11/11 11/11
iperf 3.9, 3.15, 3.16, 3.22: default and -R 16/16 16/16

Single-stream throughput with -l 500 over Ethernet, where per-read overhead shows: 35.5 Mbit/s before this PR, 35.8 after (medians of 6).

Not addressed: -P 5 needs 6 TCP connections and the port allows 5.

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.

UDP and the client side still use a single stream.

@kwagyeman

Copy link
Copy Markdown
Contributor Author

@dpgeorge - ipref is much better now.

@dpgeorge

Copy link
Copy Markdown
Member

Thanks for the PR.

I've tested this on PYBD_SF2 and it does improve the situation with large TCP buffer size (tested with -l 50000).

Support parallel TCP streams in the server

Do you think this is a useful feature to add? It adds quite a bit of code and complexity, and could slow things down on some boards (eg when testing very fast gigabit Ethernet) because now there are loops over sockets rather than just one socket.

Also, the feature doesn't work reliably for me. Sometimes it finishes but other times it times out at the end, eg on the host side:

$ iperf3 -c 10.0.0.145 -l 5000 -P 2
Connecting to host 10.0.0.145, port 5201
[  4] local 10.0.0.81 port 39896 connected to 10.0.0.145 port 5201
[  6] local 10.0.0.81 port 39908 connected to 10.0.0.145 port 5201
[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
[  4]   0.00-1.00   sec   586 KBytes  4.80 Mbits/sec    0   12.5 KBytes
[  6]   0.00-1.00   sec   586 KBytes  4.80 Mbits/sec    0   12.5 KBytes
[SUM]   0.00-1.00   sec  1.14 MBytes  9.60 Mbits/sec    0
...
...
- - - - - - - - - - - - - - - - - - - - - - - - -
[  4]   8.00-9.00   sec   601 KBytes  4.92 Mbits/sec    0   12.5 KBytes
[  6]   8.00-9.00   sec   581 KBytes  4.76 Mbits/sec    1   8.59 KBytes
[SUM]   8.00-9.00   sec  1.15 MBytes  9.68 Mbits/sec    1
warning: Failed to read JSON data size - read returned 0; errno=0
iperf3: error - unable to receive results:

I suggest either drop the parallel-streams feature, or make it reliable (hopefully without any more complexity...).

@kwagyeman
kwagyeman force-pushed the kwabena/iperf3_partial_block branch from d58b242 to 417f2e5 Compare October 28, 2026 01:20
@kwagyeman

kwagyeman commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor Author

Found the timeouts: after TEST_END the server stopped reading the data streams but kept them open, so with -P 2 their unread data filled lwIP's receive buffers and the results exchange couldn't get through. On an OPENMV_N6 that was 0/10 over WiFi and 1/10 over Ethernet. The reference server closes its streams on TEST_END; this one does now too. That also replaces the old reverse-mode "keep sending" loop.

On complexity and speed: it no longer re-creates the listening socket (fixed backlog of 4), and finding the ready stream checks the first one directly, so single-stream -l 500 is the same speed as before the PR (35.5 vs 35.8 Mbit/s). The PR is smaller overall, +61/-43. I think -P is worth keeping: it costs nothing for a single stream now and it's the easy way to load a board with several connections.

Re-creating the listener also exposed a modlwip bug: closing a listening socket writes past its pcb and corrupted the control connection, which hung about 1 in 30 -P 2 runs over WiFi. Fix in 19819.

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 now
has a backlog of 4 rather than 1.  That is also as many streams as there
are TCP pcbs for with lwIP's default of 5, after the control connection.
UDP and the client are unchanged, they still use one stream.

Signed-off-by: Kwabena W. Agyeman <kwagyeman@live.com>
The reference iperf3 server closes its data streams as soon as the client
sends TEST_END, before it asks for the client's results.  This server
kept them open until the end of the test.

When the server is receiving, the data the client still had in flight
stays queued in the network stack, because the server no longer reads
it.  With two streams that can be all of the stack's receive buffers (on
OPENMV_N6 two streams can hold 48 segments, against a pool of 32), and
then the client's results, or its acknowledgement of the server's
results, can't be received.  `iperf3 -c <board> -l 5000 -P 2` then timed
out at the end: on OPENMV_N6 in 10 of 10 runs over WiFi and 1 of 10 over
Ethernet.

When the server is sending, a client that reads whole blocks with a
blocking read (iperf 3.1.3) is left waiting for the rest of a block and
doesn't see the request for results.  The server used to keep sending
until its socket buffer was full, which is not enough once the client
has stopped reading.  With the streams closed, the client sees the end
of the stream instead.

Close the streams on TEST_END, as the reference implementation does.

Signed-off-by: Kwabena W. Agyeman <kwagyeman@live.com>

@dpgeorge dpgeorge left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updating. I tested on OPENMV_N6 and parallel streams works reliably now.

During merge I'll bump the version to 0.2.0. A minor bump is warranted now that parallel streams are supported.

Previous commits fixed bugs and implemented parallel stream support.

Signed-off-by: Kwabena W. Agyeman <kwagyeman@live.com>
@dpgeorge
dpgeorge force-pushed the kwabena/iperf3_partial_block branch from 417f2e5 to 00f72c4 Compare October 28, 2026 01:55
@dpgeorge
dpgeorge merged commit 00f72c4 into micropython:master Oct 28, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants