fix: keep wifi OTA off the heap floor (#2074)

OTA install streams the full 5.8MB image over a multi-minute TLS session
while wifi/LWIP already holds the big internal arena. Measured on
device, the arena bottomed out at ~7.7KB free with the largest
contiguous block down to ~2.4KB; for 80% of the download there wasn't
even a contiguous 8KB block. It finishes on a clean heap but tips into
OOM for anyone carrying more pre-OTA fragmentation.

Two avoidable drains, both in OtaUpdater:

- The esp_http_client RX/TX buffers were 8192/8192 on both the version
check and the install. RX only has to hold response headers (bodies
stream through the parser / OTA writer) and TX only carries our GET, so
trim both to 4096/1024. 4096 still fits the github->CDN redirect
headers; the 512 IDF default truncates them, which is why they got
oversized in the first place.

- installUpdate fired the progress callback every ~100ms perform
iteration, waking the render task on every tick. Its framebuffer work
fights the TLS session for the same arena, and epd can't really repaint
faster than a percent anyway. Throttle it to whole-percent changes.

On device, combined: floor 7.7KB -> 19KB, worstcase contiguous block
2.4KB -> 21KB, zero sub 8KB iterations across the whole download.

KOReaderSyncClient already uses small buffers; HttpDownloader is on the
Arduino HTTPClient stack with no equivalent knob, so neither changed.



Did you use AI tools to help write this code? partial, heap-exploration
assisted by Claude.
This commit is contained in:
Jeremy Klein
2026-05-20 08:46:49 -04:00
committed by GitHub
parent 252a64fa97
commit 0b9d1a7d23
+22 -9
View File
@@ -33,8 +33,11 @@ OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() {
esp_http_client_config_t client_config = {
.url = latestReleaseUrl,
.event_handler = event_handler,
.buffer_size = 8192,
.buffer_size_tx = 8192,
// 4096 holds the API response headers; the 32KB body streams through the
// parser in chunks so RX needn't be larger. TX only carries our GET.
// Both free before installUpdate, so smaller leaves it less fragmentation.
.buffer_size = 4096,
.buffer_size_tx = 1024,
.user_data = &releaseParser,
.skip_cert_common_name_check = true,
.crt_bundle_attach = esp_crt_bundle_attach,
@@ -151,12 +154,11 @@ OtaUpdater::OtaUpdaterError OtaUpdater::installUpdate(ProgressCallback onProgres
esp_http_client_config_t client_config = {
.url = otaUrl.c_str(),
.timeout_ms = 15000,
/* Default HTTP client buffer size 512 byte only
* not sufficient to handle URL redirection cases or
* parsing of large HTTP headers.
*/
.buffer_size = 8192,
.buffer_size_tx = 8192,
// 4096 holds the github->CDN redirect headers (the 512 default truncates
// them); TX only carries our GET. Both are contiguous blocks contending
// with the TLS handshake on a tight internal arena, so keep them minimal.
.buffer_size = 4096,
.buffer_size_tx = 1024,
.skip_cert_common_name_check = true,
.crt_bundle_attach = esp_crt_bundle_attach,
.keep_alive_enable = true,
@@ -176,10 +178,21 @@ OtaUpdater::OtaUpdaterError OtaUpdater::installUpdate(ProgressCallback onProgres
return INTERNAL_UPDATE_ERROR;
}
int lastReportedPct = -1;
do {
esp_err = esp_https_ota_perform(ota_handle);
processedSize = esp_https_ota_get_image_len_read(ota_handle);
if (onProgress) onProgress(ctx);
// Fire the callback only on whole-percent change. Without this it fired
// every ~100ms perform iteration, waking the render task whose framebuffer
// work contends with TLS on the same internal arena. E-ink can't repaint
// faster than a percent tick anyway.
if (onProgress && totalSize > 0) {
const int pct = static_cast<int>(static_cast<uint64_t>(processedSize) * 100 / totalSize);
if (pct != lastReportedPct) {
lastReportedPct = pct;
onProgress(ctx);
}
}
delay(100); // TODO: should we replace this with something better?
} while (esp_err == ESP_ERR_HTTPS_OTA_IN_PROGRESS);