From d1c8d45d5dc8ae09f03e2f0a9c6b3ac1910b8cdc Mon Sep 17 00:00:00 2001 From: Luke Hoersten Date: Sat, 20 Jun 2026 20:50:43 -0500 Subject: firmware: split stream recv into its own task with 3-buffer ping-pong MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handle_client previously ran recv → decode → paint serially on one FreeRTOS task. The kernel TCP buffer filled during decode+paint (~6ms), and against the IDF-default 5760-byte window the sender naturally stop-go-rate-limited to ~consumption. Raising the window to 65535 (previous experiment) regressed g2g from ~100ms to 17s growing unbounded — the sender pumped 45+ segments per round into a kernel buffer the app couldn't drain in time, and there was no way to skip-oldest on the kernel queue. This commit decouples recv from decode+paint: recv-task: owns the socket. Reads header + body into one of three preallocated PSRAM body buffers. On body complete, swaps the just-filled buffer into a 1-deep pending slot and picks a free buffer for the next recv. If the slot already held a frame (decode is slow), drops oldest in place — mirror of the Scrypted-side skip-oldest from e5acf93. decode-task: waits on a binary semaphore. On signal, claims pending, then decodes + paints without holding any shared lock. Frees its prior buffer implicitly by overwriting s_decode_idx on the next claim. 3 PSRAM body buffers (~3MB of 28MB free) ensure the invariant {recv_idx, pending_idx, decode_idx} are pairwise distinct without ever blocking recv. jpeg_decoder.c grew an alloc_input_buffer helper + jpeg_decoder_decode now takes an explicit input pointer so the stream and http_api snapshot paths don't share scratch. New stats: - recv_dropped_oldest: per-window count of pending-slot overwrites - decode_idle_min/avg/max_us: time decode-task spent waiting on signal Measurement at IDF-default 5760 window, Unifi medium substream: before split: recv_avg=32ms recv_max~44ms fps=22-26 (recv blocked during 6ms decode+paint; chunk_max capped at 5760) after split: recv_avg=17ms recv_max=18-37ms fps=21-29 steady, decode_idle_avg=27-40ms (decode mostly waiting), drop_oldest=0, painted at source rate The bottleneck moved from 'decode+paint serializes recv' to the wire's own send rate. Bigger windows are now safe (recv-task drains continuously, can't bury us), but won't add fps until source rate goes up — that's a separate conversation. --- main/http_api.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'main/http_api.c') diff --git a/main/http_api.c b/main/http_api.c index ccd795b..3774039 100644 --- a/main/http_api.c +++ b/main/http_api.c @@ -112,6 +112,10 @@ static esp_err_t state_get_handler(httpd_req_t *req) cJSON_AddNumberToObject(s, "recv_chunk_avg", (double)stream.recv_chunk_avg); cJSON_AddNumberToObject(s, "recv_chunk_max", (double)stream.recv_chunk_max); cJSON_AddNumberToObject(s, "so_rcvbuf", (double)stream.so_rcvbuf); + cJSON_AddNumberToObject(s, "recv_dropped_oldest", (double)stream.recv_dropped_oldest); + cJSON_AddNumberToObject(s, "decode_idle_min_us", (double)stream.decode_idle_min_us); + cJSON_AddNumberToObject(s, "decode_idle_avg_us", (double)stream.decode_idle_avg_us); + cJSON_AddNumberToObject(s, "decode_idle_max_us", (double)stream.decode_idle_max_us); cJSON_AddNumberToObject(s, "last_paint_event_us_low", (double)stream.last_paint_event_us_low); cJSON_AddItemToObject(root, "stream", s); @@ -424,7 +428,7 @@ static esp_err_t frame_post_handler(httpd_req_t *req) size_t back_size = 0; void *back = display_back_buffer(&back_size); uint16_t w = 0, h = 0; - esp_err_t dec_err = jpeg_decoder_decode(got, back, back_size, &w, &h); + esp_err_t dec_err = jpeg_decoder_decode(in, got, back, back_size, &w, &h); int64_t t_decode = esp_timer_get_time(); if (dec_err != ESP_OK) { viewport_state_lock(); -- cgit v1.2.3