From 70f9b548496ddc9d20b3086b250479efb432371d Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Thu, 1 Oct 2026 22:26:18 -0700 Subject: [PATCH 1/5] iperf3: Don't wait for a full block when receiving TCP data. 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 ` 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 --- python-ecosys/iperf3/iperf3.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/python-ecosys/iperf3/iperf3.py b/python-ecosys/iperf3/iperf3.py index 05d69f774..ccb7e545f 100644 --- a/python-ecosys/iperf3/iperf3.py +++ b/python-ecosys/iperf3/iperf3.py @@ -217,8 +217,10 @@ def _transfer(udp, reverse, addr, s_data, buf, udp_last_send, udp_packet_id, udp stats.add_bytes(n) else: if reverse: - recvninto(s_data, buf) - n = len(buf) + # The socket is non-blocking, so this reads only what is available. The + # sender is not required to end the stream on a block boundary, so waiting + # here for a full block could block forever and miss TEST_END. + n = recvinto(s_data, buf) or 0 else: n = s_data.send(buf) stats.add_bytes(n) @@ -260,6 +262,9 @@ def server_once(): s_data, addr = s_listen.accept() print("Accepted connection:", addr) recvn(s_data, COOKIE_SIZE) + if not reverse: + # Receive without blocking, see _transfer(). + s_data.setblocking(False) udp = False udp_packet_id = 0 udp_interval = None @@ -488,6 +493,9 @@ def client(host, udp=False, reverse=False, bandwidth=10 * 1024 * 1024): s_data = socket.socket(ai[0], socket.SOCK_STREAM) s_data.connect(ai[-1]) s_data.sendall(cookie) + if reverse: + # Receive without blocking, see _transfer(). + s_data.setblocking(False) buf = bytearray(urandom(param["len"])) elif cmd == EXCHANGE_RESULTS: # Close data socket now that server knows we are finished, to prevent it flooding us From d3c12b63780a2d4e7304be255dacebe0a7b453b0 Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Fri, 2 Oct 2026 08:48:49 -0700 Subject: [PATCH 2/5] iperf3: Accept clients that do not send a pacing timer. 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 --- python-ecosys/iperf3/iperf3.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python-ecosys/iperf3/iperf3.py b/python-ecosys/iperf3/iperf3.py index ccb7e545f..2be7b58f2 100644 --- a/python-ecosys/iperf3/iperf3.py +++ b/python-ecosys/iperf3/iperf3.py @@ -71,7 +71,8 @@ def fmt_size(val, div): class Stats: def __init__(self, param): - self.pacing_timer_us = param["pacing_timer"] * 1000 + # iperf3 clients before 3.2 do not send the pacing timer. + self.pacing_timer_us = param.get("pacing_timer", 1000) * 1000 self.udp = param.get("udp", False) self.reverse = param.get("reverse", False) self.running = False From 8df15b9d80312b91cf368f50f31ac1d2adc5567b Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Fri, 2 Oct 2026 09:08:16 -0700 Subject: [PATCH 3/5] iperf3: Support parallel TCP streams in the server. The server accepted a single data connection whatever the client asked for, so `iperf3 -c -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 --- python-ecosys/iperf3/iperf3.py | 105 ++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 40 deletions(-) diff --git a/python-ecosys/iperf3/iperf3.py b/python-ecosys/iperf3/iperf3.py index 2be7b58f2..9bf472000 100644 --- a/python-ecosys/iperf3/iperf3.py +++ b/python-ecosys/iperf3/iperf3.py @@ -228,15 +228,20 @@ def _transfer(udp, reverse, addr, s_data, buf, udp_last_send, udp_packet_id, udp return udp_last_send, udp_packet_id +def _listen(ai, backlog): + s_listen = socket.socket(ai[0], socket.SOCK_STREAM) + s_listen.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s_listen.bind(ai[-1]) + s_listen.listen(backlog) + return s_listen + + def server_once(): # Listen for a connection ai = socket.getaddrinfo("0.0.0.0", 5201) ai = ai[0] print("Server listening on", ai[-1]) - s_listen = socket.socket(ai[0], socket.SOCK_STREAM) - s_listen.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s_listen.bind(ai[-1]) - s_listen.listen(1) + s_listen = _listen(ai, 1) s_ctrl, addr = s_listen.accept() # Read client's cookie @@ -254,18 +259,28 @@ def server_once(): if DEBUG: print(param) reverse = param.get("reverse", False) + parallel = param.get("parallel", 1) + + if parallel > 1: + # The client opens all of its streams at once, so listen again with room to + # queue them all. + s_listen.close() + s_listen = _listen(ai, parallel) # Ask to create streams s_ctrl.sendall(bytes([CREATE_STREAMS])) if param.get("tcp", False): - # Accept stream - s_data, addr = s_listen.accept() - print("Accepted connection:", addr) - recvn(s_data, COOKIE_SIZE) - if not reverse: - # Receive without blocking, see _transfer(). - s_data.setblocking(False) + # Accept streams, the client opens one connection per parallel stream + s_data = [] + for _ in range(parallel): + s, addr = s_listen.accept() + print("Accepted connection:", addr) + recvn(s, COOKIE_SIZE) + if not reverse: + # Receive without blocking, see _transfer(). + s.setblocking(False) + s_data.append(s) udp = False udp_packet_id = 0 udp_interval = None @@ -273,10 +288,11 @@ def server_once(): elif param.get("udp", False): # Close TCP connection and open UDP "connection" s_listen.close() - s_data = socket.socket(ai[0], socket.SOCK_DGRAM) - s_data.bind(ai[-1]) - data, addr = s_data.recvfrom(4) - s_data.sendto(struct.pack(" Date: Fri, 2 Oct 2026 09:35:16 -0700 Subject: [PATCH 4/5] iperf3: Keep sending in reverse mode until the client responds. 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 --- python-ecosys/iperf3/iperf3.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/python-ecosys/iperf3/iperf3.py b/python-ecosys/iperf3/iperf3.py index 9bf472000..5c8b1b176 100644 --- a/python-ecosys/iperf3/iperf3.py +++ b/python-ecosys/iperf3/iperf3.py @@ -343,23 +343,28 @@ def server_once(): break stats.update() - # Need to continue writing so other side doesn't get blocked waiting for data - if reverse and not udp: - poll.unregister(s_ctrl) - while True: - pollables = poll.poll(0) - if not pollables: - break - for pollable in pollables: - for s in s_data: - if pollable_is_sock(pollable, s): - s.send(data_buf) - stats.stop() # Ask to exchange results s_ctrl.sendall(bytes([EXCHANGE_RESULTS])) + # Need to continue writing until the other side responds, because it may be blocked + # waiting for the rest of a block of data and not see the request until it gets it + if reverse and not udp: + waiting = True + while waiting: + for pollable in poll.poll(): + if pollable_is_sock(pollable, s_ctrl): + waiting = False + continue + for s in s_data: + if pollable_is_sock(pollable, s): + try: + s.send(data_buf) + except OSError: + # The other side has closed this stream + poll.unregister(s) + # Get client results n = struct.unpack(">I", recvn(s_ctrl, 4))[0] results = recvn(s_ctrl, n) From d58b24230f3fda30ba52a53a3e53011d54d25667 Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Fri, 2 Oct 2026 09:35:16 -0700 Subject: [PATCH 5/5] iperf3: Bump version to 0.1.6. Signed-off-by: Kwabena W. Agyeman --- python-ecosys/iperf3/manifest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-ecosys/iperf3/manifest.py b/python-ecosys/iperf3/manifest.py index d63c2d6d1..8c1570d14 100644 --- a/python-ecosys/iperf3/manifest.py +++ b/python-ecosys/iperf3/manifest.py @@ -1,3 +1,3 @@ -metadata(version="0.1.5", pypi="iperf3", pypi_publish="uiperf3") +metadata(version="0.1.6", pypi="iperf3", pypi_publish="uiperf3") module("iperf3.py")