From 3d3456e9e81502a477f6498c44d0691499dda8f9 Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Fri, 13 Dec 2024 16:23:09 +0900 Subject: [PATCH 1/5] implement in-flight buffer credits and event moderation for large/chunked responses Referer to https://github.com/mathieucarbou/ESPAsyncWebServer/discussions/165 Relates to #169 in-flight buffer credits are intended to moderate buffer fill callbacks in AsyncAbstractResponse it could prevent bad designed slow user-callbacks to flood the queue in chunked responces. for response data we need to control the queue and in-flight fragmentation. Sending small chunks could give low latency, but flood asynctcp's queue and fragment socket buffer space for large responses. Let's ignore polled acks and acks in case when we have more in-flight data then the available socket buff space. That way we could balance on having half the buffer in-flight while another half is filling up, while minimizing events in asynctcp q --- src/WebResponseImpl.h | 4 ++++ src/WebResponses.cpp | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/WebResponseImpl.h b/src/WebResponseImpl.h index b58c5bb..fa462b6 100644 --- a/src/WebResponseImpl.h +++ b/src/WebResponseImpl.h @@ -47,6 +47,10 @@ class AsyncBasicResponse : public AsyncWebServerResponse { class AsyncAbstractResponse : public AsyncWebServerResponse { private: + // amount of responce data in-flight, i.e. sent, but not acked yet + size_t _in_flight{0}; + // in-flight queue credits + size_t _in_flight_credit{2}; String _head; // Data is inserted into cache at begin(). // This is inefficient with vector, but if we use some other container, diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp index 7a26e92..f4870cd 100644 --- a/src/WebResponses.cpp +++ b/src/WebResponses.cpp @@ -352,7 +352,21 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest* request, size_t len, u request->client()->close(); return 0; } + // return a credit for each chunk of acked data (polls does not give any credits) + if (len) + ++_in_flight_credit; + + // for chunked responses ignore acks if there are no _in_flight_credits left + if (_chunked && !_in_flight_credit){ +#ifdef ESP32 + log_d("(chunk) out of in-flight credits"); +#endif + return 0; + } + _ackedLength += len; + _in_flight -= (_in_flight > len) ? len : _in_flight; + // get the size of available sock space size_t space = request->client()->space(); size_t headLen = _head.length(); @@ -364,16 +378,31 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest* request, size_t len, u String out = _head.substring(0, space); _head = _head.substring(space); _writtenLength += request->client()->write(out.c_str(), out.length()); + _in_flight += out.length(); + --_in_flight_credit; // take a credit return out.length(); } } if (_state == RESPONSE_CONTENT) { + // for response data we need to control the queue and in-flight fragmentation. Sending small chunks could give low latency, + // but flood asynctcp's queue and fragment socket buffer space for large responses. + // Let's ignore polled acks and acks in case when we have more in-flight data then the available socket buff space. + // That way we could balance on having half the buffer in-flight while another half is filling up, while minimizing events in asynctcp q + if (_in_flight > space){ + //log_d("defer user call %u/%u", _in_flight, space); + // take the credit back since we are ignoring this ack and rely on other inflight data + if (len) + --_in_flight_credit; + return 0; + } + size_t outLen; if (_chunked) { if (space <= 8) { return 0; } + outLen = space; } else if (!_sendContentLength) { outLen = space; @@ -422,6 +451,8 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest* request, size_t len, u if (outLen) { _writtenLength += request->client()->write((const char*)buf, outLen); + _in_flight += outLen; + --_in_flight_credit; // take a credit } if (_chunked) { From 92cdaa16b4533d7b70bfbed5979fbd00513a33f4 Mon Sep 17 00:00:00 2001 From: Mathieu Carbou Date: Sun, 15 Dec 2024 16:24:03 +0100 Subject: [PATCH 2/5] update issue --- .github/ISSUE_TEMPLATE/bug_report.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8813107..895387e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -32,3 +32,9 @@ As an alternative, you can use [https://maximeborges.github.io/esp-stacktrace-de **Additional notes** Add any other context about the problem here. + +# WARNING + +**ANY ISSUE POSTED WITH AN ENCODED STACK TRACE WILL BE CLOSED !** + +**PLEASE MAKE THE EFFORT TO DECODE YOUR STACK TRACE** From c1e3bf06657ac1601c1e2433fe4a356dde835c9a Mon Sep 17 00:00:00 2001 From: Mathieu Carbou Date: Sun, 15 Dec 2024 17:34:05 +0100 Subject: [PATCH 3/5] clang reformat --- src/WebResponses.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp index f4870cd..bf8235e 100644 --- a/src/WebResponses.cpp +++ b/src/WebResponses.cpp @@ -357,7 +357,7 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest* request, size_t len, u ++_in_flight_credit; // for chunked responses ignore acks if there are no _in_flight_credits left - if (_chunked && !_in_flight_credit){ + if (_chunked && !_in_flight_credit) { #ifdef ESP32 log_d("(chunk) out of in-flight credits"); #endif @@ -379,7 +379,7 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest* request, size_t len, u _head = _head.substring(space); _writtenLength += request->client()->write(out.c_str(), out.length()); _in_flight += out.length(); - --_in_flight_credit; // take a credit + --_in_flight_credit; // take a credit return out.length(); } } @@ -389,9 +389,9 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest* request, size_t len, u // but flood asynctcp's queue and fragment socket buffer space for large responses. // Let's ignore polled acks and acks in case when we have more in-flight data then the available socket buff space. // That way we could balance on having half the buffer in-flight while another half is filling up, while minimizing events in asynctcp q - if (_in_flight > space){ - //log_d("defer user call %u/%u", _in_flight, space); - // take the credit back since we are ignoring this ack and rely on other inflight data + if (_in_flight > space) { + // log_d("defer user call %u/%u", _in_flight, space); + // take the credit back since we are ignoring this ack and rely on other inflight data if (len) --_in_flight_credit; return 0; @@ -452,7 +452,7 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest* request, size_t len, u if (outLen) { _writtenLength += request->client()->write((const char*)buf, outLen); _in_flight += outLen; - --_in_flight_credit; // take a credit + --_in_flight_credit; // take a credit } if (_chunked) { From 1756680088d5e71c9a811a06ba2ea5272faebc2e Mon Sep 17 00:00:00 2001 From: Mathieu Carbou Date: Sun, 15 Dec 2024 17:34:51 +0100 Subject: [PATCH 4/5] pointing to https://github.com/mathieucarbou/AsyncTCP#coalescedq --- platformio.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 4a59440..49a9ba9 100644 --- a/platformio.ini +++ b/platformio.ini @@ -31,7 +31,8 @@ lib_deps = ; bblanchon/ArduinoJson @ 5.13.4 ; bblanchon/ArduinoJson @ 6.21.5 bblanchon/ArduinoJson @ 7.2.1 - mathieucarbou/AsyncTCP @ 3.2.15 + ; mathieucarbou/AsyncTCP @ 3.2.15 + https://github.com/mathieucarbou/AsyncTCP#coalescedq board = esp32dev board_build.partitions = partitions-4MB.csv board_build.filesystem = littlefs From 0dce607ef3fe45dc3f72a368a8b04ea464a28108 Mon Sep 17 00:00:00 2001 From: Mathieu Carbou Date: Sun, 15 Dec 2024 18:24:42 +0100 Subject: [PATCH 5/5] mathieucarbou/AsyncTCP @ 3.3.0 --- .github/workflows/ci.yml | 2 +- README.md | 8 ++++---- docs/index.md | 8 ++++---- library.json | 2 +- platformio.ini | 7 +++---- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fe2073..c319338 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: - name: Install AsyncTCP (ESP32) if: ${{ matrix.core == 'esp32:esp32' }} - run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/mathieucarbou/AsyncTCP#v3.2.15 + run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/mathieucarbou/AsyncTCP#v3.3.0 - name: Install ESPAsyncTCP (ESP8266) if: ${{ matrix.core == 'esp8266:esp8266' }} diff --git a/README.md b/README.md index de4f91a..8e2d2b3 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,8 @@ lib_deps = mathieucarbou/ESPAsyncWebServer @ 3.4.0 **Dependencies:** -- **ESP32 with AsyncTCP**: `mathieucarbou/AsyncTCP @ 3.2.15` - Arduino IDE: [https://github.com/mathieucarbou/AsyncTCP#v3.2.15](https://github.com/mathieucarbou/AsyncTCP/releases) +- **ESP32 with AsyncTCP**: `mathieucarbou/AsyncTCP @ 3.3.0` + Arduino IDE: [https://github.com/mathieucarbou/AsyncTCP#v3.3.0](https://github.com/mathieucarbou/AsyncTCP/releases) - **ESP32 with AsyncTCPSock**: `https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip` @@ -99,7 +99,7 @@ AsyncTCPSock can be used instead of AsyncTCP by excluding AsyncTCP from the libr lib_compat_mode = strict lib_ldf_mode = chain lib_deps = - ; mathieucarbou/AsyncTCP @ 3.2.15 + ; mathieucarbou/AsyncTCP @ 3.3.0 https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip mathieucarbou/ESPAsyncWebServer @ 3.4.0 lib_ignore = @@ -116,7 +116,7 @@ Performance of `mathieucarbou/ESPAsyncWebServer @ 3.4.0`: > autocannon -c 10 -w 10 -d 20 http://192.168.4.1 ``` -With `mathieucarbou/AsyncTCP @ 3.2.15` +With `mathieucarbou/AsyncTCP @ 3.3.0` [![](https://mathieu.carbou.me/ESPAsyncWebServer/perf-c10.png)](https://mathieu.carbou.me/ESPAsyncWebServer/perf-c10.png) diff --git a/docs/index.md b/docs/index.md index de4f91a..8e2d2b3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -80,8 +80,8 @@ lib_deps = mathieucarbou/ESPAsyncWebServer @ 3.4.0 **Dependencies:** -- **ESP32 with AsyncTCP**: `mathieucarbou/AsyncTCP @ 3.2.15` - Arduino IDE: [https://github.com/mathieucarbou/AsyncTCP#v3.2.15](https://github.com/mathieucarbou/AsyncTCP/releases) +- **ESP32 with AsyncTCP**: `mathieucarbou/AsyncTCP @ 3.3.0` + Arduino IDE: [https://github.com/mathieucarbou/AsyncTCP#v3.3.0](https://github.com/mathieucarbou/AsyncTCP/releases) - **ESP32 with AsyncTCPSock**: `https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip` @@ -99,7 +99,7 @@ AsyncTCPSock can be used instead of AsyncTCP by excluding AsyncTCP from the libr lib_compat_mode = strict lib_ldf_mode = chain lib_deps = - ; mathieucarbou/AsyncTCP @ 3.2.15 + ; mathieucarbou/AsyncTCP @ 3.3.0 https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip mathieucarbou/ESPAsyncWebServer @ 3.4.0 lib_ignore = @@ -116,7 +116,7 @@ Performance of `mathieucarbou/ESPAsyncWebServer @ 3.4.0`: > autocannon -c 10 -w 10 -d 20 http://192.168.4.1 ``` -With `mathieucarbou/AsyncTCP @ 3.2.15` +With `mathieucarbou/AsyncTCP @ 3.3.0` [![](https://mathieu.carbou.me/ESPAsyncWebServer/perf-c10.png)](https://mathieu.carbou.me/ESPAsyncWebServer/perf-c10.png) diff --git a/library.json b/library.json index 804e915..e37ab94 100644 --- a/library.json +++ b/library.json @@ -28,7 +28,7 @@ { "owner": "mathieucarbou", "name": "AsyncTCP", - "version": "^3.2.15", + "version": "^3.3.0", "platforms": "espressif32" }, { diff --git a/platformio.ini b/platformio.ini index 49a9ba9..099d5bc 100644 --- a/platformio.ini +++ b/platformio.ini @@ -31,8 +31,7 @@ lib_deps = ; bblanchon/ArduinoJson @ 5.13.4 ; bblanchon/ArduinoJson @ 6.21.5 bblanchon/ArduinoJson @ 7.2.1 - ; mathieucarbou/AsyncTCP @ 3.2.15 - https://github.com/mathieucarbou/AsyncTCP#coalescedq + mathieucarbou/AsyncTCP @ 3.3.0 board = esp32dev board_build.partitions = partitions-4MB.csv board_build.filesystem = littlefs @@ -50,7 +49,7 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/ ; board = esp32-s3-devkitc-1 ; board = esp32-c6-devkitc-1 lib_deps = - mathieucarbou/AsyncTCP @ 3.2.15 + mathieucarbou/AsyncTCP @ 3.3.0 [env:arduino-310] platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc3/platform-espressif32.zip @@ -103,7 +102,7 @@ board = ${sysenv.PIO_BOARD} platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip board = ${sysenv.PIO_BOARD} lib_deps = - mathieucarbou/AsyncTCP @ 3.2.15 + mathieucarbou/AsyncTCP @ 3.3.0 [env:ci-arduino-310] platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc3/platform-espressif32.zip