Skip to content

Commit 6ef43e0

Browse files
committed
WebResponses: Simplify packet buffer allocation
When packet buffer allocation fails, try using TCP_MSS (one packet's worth) instead of reaching for the second-largest free block. This can improve the low memory behavior and simplifies the logic quite a bit. Also use the correct available space estimator on ESP32, and minify the debug logs to reduce the performance impact when tracing.
1 parent 0d73dd8 commit 6ef43e0

1 file changed

Lines changed: 51 additions & 42 deletions

File tree

src/WebResponses.cpp

Lines changed: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,6 @@
3030
#define DEBUG_PRINTFP(...)
3131
#endif
3232

33-
#ifdef ESP8266
34-
#define GET_MAX_BLOCK_SIZE getMaxFreeBlockSize
35-
#else
36-
#define GET_MAX_BLOCK_SIZE getMaxAllocHeap
37-
#endif
38-
// When looking up available memory, leave some slack
39-
#define BLOCK_SIZE_SLACK 128
40-
4133

4234
/*
4335
* Abstract Response
@@ -291,14 +283,40 @@ void AsyncAbstractResponse::_respond(AsyncWebServerRequest *request){
291283
_ack(request, 0, 0);
292284
}
293285

294-
template<typename T>
295-
static void dealloc_vector(T& vec) {
296-
vec = T{}; // move construct an empty vector; space will be freed when scope exits
286+
287+
static size_t _max_heap_alloc() {
288+
auto result =
289+
#ifdef ESP8266
290+
ESP.getMaxFreeBlockSize()
291+
#else
292+
// ESP.getMaxAllocHeap() does not accurately reflect the behavior of malloc()
293+
// as it is missing the 'MALLOC_CAP_DEFAULT' flag.
294+
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_DEFAULT)
295+
#endif
296+
- 128; // fudge factor
297+
298+
return result;
299+
}
300+
301+
static DynamicBuffer _safe_allocate_buffer(size_t outLen) {
302+
// Espressif lwip configuration always copies in to the TCP stack, so
303+
// we have to have enough room to allocate the copy buffer. It's too bad we
304+
// can't re-use our assembly buffer, but it is what it is.
305+
auto rv = DynamicBuffer(outLen);
306+
if (outLen > TCP_MSS) {
307+
// Validate that there's enough space to allocate the copy buffer
308+
if (!rv || (_max_heap_alloc() < outLen)) {
309+
// Try allocating a single packet's worth instead
310+
rv.clear();
311+
rv.resize(TCP_MSS);
312+
}
313+
}
314+
return rv;
297315
}
298316

299317
size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time){
300318
(void)time;
301-
DEBUG_PRINTFP("(%d) ack %d\n", (intptr_t) this, len);
319+
DEBUG_PRINTFP("(%08x) ack %d\n", (intptr_t) this, len);
302320

303321
if(!_sourceValid()){
304322
_state = RESPONSE_FAILED;
@@ -307,16 +325,17 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u
307325
}
308326
_ackedLength += len;
309327

310-
size_t space = request->client()->space(); // TCP window space available; NOT a guarantee we can actually send this much
311-
size_t headLen = _head.length();
328+
size_t space = request->client()->space(); // TCP window space available; NOT a guarantee we can actually send this much
312329
bool needs_send = false;
313330
if ((space == 0) && ((_state == RESPONSE_HEADERS) || (_state == RESPONSE_CONTENT))) {
314331
// Cannot accept more data now, wait for next event
315-
DEBUG_PRINTFP("(%d) No space to write\n", (intptr_t)this);
332+
DEBUG_PRINTFP("(%08x)NS\n", (intptr_t)this);
316333
return 0;
317334
}
318335

319336
if(_state == RESPONSE_HEADERS){
337+
// FUTURE: we could replace _head with _packet
338+
size_t headLen = _head.length();
320339
auto headWritten = request->client()->add(_head.c_str(), std::min(space, headLen));
321340
_writtenLength += headWritten;
322341
if (headWritten < headLen) {
@@ -339,14 +358,15 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u
339358
space -= written;
340359
if (_packet.size()) {
341360
// Couldn't queue the full cache
342-
DEBUG_PRINTFP("(%d) Partial buffered write: wrote %d, remaining %d\n", (intptr_t)this, written, _packet.size());
361+
DEBUG_PRINTFP("(%08x)PBW %d,%d\n", (intptr_t)this, written, _packet.size());
343362
if (written) request->client()->send();
344363
return written;
345364
}
365+
_packet = {};
346366
needs_send = true;
347367
}
348-
_packet.reset();
349-
368+
assert(_packet.capacity() == 0); // no buffer is allocated
369+
350370
size_t outLen, readLen;
351371
if(_chunked){
352372
if(space <= 8){
@@ -361,27 +381,16 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u
361381

362382
// Limit outlen based on available memory
363383
// We require two packet buffers - one allocated here, and one belonging to the TCP stack
364-
{
365-
auto old_space = _packet.capacity();
366-
auto max_block_size = ESP.GET_MAX_BLOCK_SIZE() - BLOCK_SIZE_SLACK;
367-
if ((old_space < outLen) || (outLen > max_block_size)) {
368-
DEBUG_PRINTFP("(%d) Space adjustment, have %d, want %d, avail %d\n", (intptr_t)this, old_space, outLen, max_block_size);
369-
do {
370-
dealloc_vector(_packet);
371-
outLen = std::min(outLen, max_block_size);
372-
_packet.reallocate(outLen);
373-
max_block_size = ESP.GET_MAX_BLOCK_SIZE() - BLOCK_SIZE_SLACK;
374-
DEBUG_PRINTFP("(%d) Checking %d vs %d\n", (intptr_t)this, outLen, max_block_size);
375-
} while (max_block_size < outLen);
376-
} else {
377-
_packet.reallocate(outLen);
378-
}
379-
}
384+
_packet = _safe_allocate_buffer(outLen);
380385

381-
if(_chunked){
386+
if(_chunked){
387+
if (_packet.size() < 8) {
388+
_packet.clear();
389+
goto content_abort;
390+
}
382391
// HTTP 1.1 allows leading zeros in chunk length. Or spaces may be added.
383392
// See RFC2616 sections 2, 3.6.1.
384-
readLen = _fillBufferAndProcessTemplates((uint8_t*) (_packet.data() + 6), outLen - 8);
393+
readLen = _fillBufferAndProcessTemplates((uint8_t*) (_packet.data() + 6), _packet.size() - 8);
385394
if(readLen == RESPONSE_TRY_AGAIN){
386395
_packet.clear();
387396
goto content_abort;
@@ -408,15 +417,15 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u
408417
_writtenLength += acceptedLen;
409418
_packet.advance(acceptedLen);
410419
if (acceptedLen < outLen) {
411-
// Save the unsent block in cache
412-
DEBUG_PRINTFP("(%d) Incomplete write, %d/%d\nHeap: %d/%d\nSpace:%d\n", (intptr_t) this, acceptedLen, outLen, ESP.GET_MAX_BLOCK_SIZE(), ESP.getFreeHeap(), request->client()->space());
413-
// Try again, with less
414-
acceptedLen = request->client()->write((const char*)_packet.data(), _packet.size()/2);
420+
DEBUG_PRINTFP("(%08x)IW%d/%d\nH:%d/%d\nS:%d\n", (intptr_t) this, acceptedLen, outLen, _max_heap_alloc(), ESP.getFreeHeap(), request->client()->space());
421+
// Try again, with less.
422+
acceptedLen = request->client()->write((const char*)_packet.data(), std::min(outLen/2, (size_t)TCP_MSS));
415423
_writtenLength += acceptedLen;
416424
_packet.advance(acceptedLen);
417425
}
418-
DEBUG_PRINTFP("(%d) Accepted: %d\n", (intptr_t) this, acceptedLen);
419-
if (_packet.size() == 0) dealloc_vector(_packet);
426+
// Data we couldn't send is held in _packet
427+
DEBUG_PRINTFP("(%08x)AL%d %d\n", (intptr_t) this, acceptedLen, _packet.size());
428+
if (_packet.size() == 0) _packet = {}; // release buffer
420429
}
421430

422431
if( (_chunked && readLen == 0) // Chunked mode, no more data

0 commit comments

Comments
 (0)