Skip to content

extmod/modlwip: Poll the stack while waiting for TCP send memory. - #19627

Open
kwagyeman wants to merge 1 commit into
micropython:masterfrom
kwagyeman:kwabena/modlwip_send_retry
Open

extmod/modlwip: Poll the stack while waiting for TCP send memory.#19627
kwagyeman wants to merge 1 commit into
micropython:masterfrom
kwagyeman:kwabena/modlwip_send_retry

Conversation

@kwagyeman

@kwagyeman kwagyeman commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Summary

When tcp_write() returns ERR_MEM, lwip_tcp_send() retries on a flat
50ms delay:

err = tcp_write(socket->pcb.tcp, buf, write_len, TCP_WRITE_FLAG_COPY);
if (err != ERR_MEM) {
    break;
}
...
mp_hal_delay_ms(50);

Nothing drives lwIP forward during that sleep, so the condition the retry
is waiting for — ACKs arriving and freeing queued segments — is only
resolved by background polling. Every write that takes this path stalls
for the full 50ms, and on any configuration whose lwIP heap is small
relative to TCP_SND_BUF this becomes the steady state: TCP transmit
throughput is silently capped at roughly write_size per 50ms, with no
error reported to the caller.

Replace the blind sleep with a 1ms delay plus poll_sockets() — the same
helper the sndbuf wait loop earlier in the function already uses.

Measured on an OpenMV RT1060 (CYW4343W WiFi, 2.4GHz) with a configuration
whose heap is smaller than the send buffer (MEM_SIZE=10K,
TCP_SND_BUF=8*MSS), sustained TCP transmit from a Python socket benchmark
using 8KB writes:

master this PR
TCP TX sustained 1.3 Mbit/s (dead constant) 14.4 Mbit/s (11x)

The 1.3 Mbit/s figure is exactly 8KB per 50ms — the sleep, not the
network, sets the throughput. Configurations whose heap comfortably covers
the send buffer never enter this path and are unaffected (verified: an
adequately-provisioned config on the same board measures identically
before and after this change).

Testing

Runtime tested on an OpenMV RT1060 (CYW4343W WiFi) in both the
heap-constrained configuration above (numbers in the table) and an
adequately-provisioned configuration (no regression, path not taken).
TCP receive and UDP in both directions are unaffected in both
configurations.

Build-tested: NUCLEO_F767ZI, OPENMV_RT1060.

Generative AI

I used generative AI tools when creating this PR, but a human has checked
the code and is responsible for the code and the description above.

@kwagyeman

Copy link
Copy Markdown
Contributor Author

@dpgeorge - Found a large issue with TCP performance loss that affects all boards.

@kwagyeman kwagyeman moved this to In progress in OpenMV Features Aug 16, 2026
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.58%. Comparing base (791ba6e) to head (f928098).
⚠️ Report is 20 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #19627   +/-   ##
=======================================
  Coverage   98.58%   98.58%           
=======================================
  Files         182      182           
  Lines       23322    23322           
  Branches        5        5           
=======================================
  Hits        22993    22993           
  Misses        328      328           
  Partials        1        1           
Flag Coverage Δ
unix-coverage-32bit 98.59% <ø> (ø)
unix-coverage-64bit 98.52% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Code size report:

Reference:  esp32/modmachine: Use popcount to count wake pins. [8cf130d]
Comparison: extmod/modlwip: Poll the stack while waiting for TCP send memory. [merge of f928098]
  mpy-cross:    +0 +0.000% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
      esp32:    +0 +0.000% ESP32_GENERIC
     mimxrt:    +0 +0.000% TEENSY40
        rp2:    +8 +0.001% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

@dpgeorge dpgeorge added the extmod Relates to extmod/ directory in source label Aug 17, 2026
Comment thread extmod/modlwip.c
// instead of sleeping blindly: a flat 50ms delay caps throughput at
// write_size/50ms whenever the heap is smaller than the send buffer.
mp_hal_delay_ms(1);
poll_sockets();

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.

I think this can just be poll_sockets(), and remove the mp_hal_delay_ms(1). Because poll_sockets() already does a 1ms delay.

Also, no need for the comment. The 50ms no longer means anything.

But, the for loop itself needs updating to use mp_hal_ticks_ms() to timeout after 10s (similar to the other waiting loop above).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

@kwagyeman
kwagyeman force-pushed the kwabena/modlwip_send_retry branch from df1add1 to 689b2e6 Compare August 17, 2026 23:11
Comment thread extmod/modlwip.c Outdated
if (err != ERR_OK) {
break;
}
if (mp_hal_ticks_ms() - write_start > 10000) {

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.

I suggest making the constant explicitly unsigned via 10000U, just to be on the safe side with the wrap around logic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

When tcp_write() returns ERR_MEM, lwip_tcp_send() retried on a flat
50ms delay.  During that sleep nothing drives lwIP forward, so the
condition the retry is waiting for (ACKs arriving and freeing queued
segments) is only resolved by background polling, and every write that
hits this path is stalled for a fixed 50ms.  On any configuration whose
lwIP heap is small relative to TCP_SND_BUF this becomes the dominant
cost and caps TCP transmit throughput at roughly write_size per 50ms,
with the socket reporting no error at all.

Poll the stack via poll_sockets() (which includes a 1ms wait) instead
of sleeping blindly, and time the retry loop out after 10 seconds using
mp_hal_ticks_ms(), matching the sndbuf waiting loop above.

Measured on an OpenMV RT1060 (CYW4343W WiFi, MEM_SIZE smaller than
TCP_SND_BUF): sustained TCP transmit from a Python socket benchmark
sat at a constant 1.3Mbit/s -- exactly its 8KB writes paced by the 50ms
sleep.  With this change the same benchmark reaches 14.4Mbit/s, and
configurations with adequately sized heaps are unaffected.

Signed-off-by: Kwabena W. Agyeman <kwagyeman@live.com>
@kwagyeman
kwagyeman force-pushed the kwabena/modlwip_send_retry branch from 689b2e6 to f928098 Compare August 25, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

extmod Relates to extmod/ directory in source

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

2 participants