diff --git a/src/main.c b/src/main.c index cba576e..2a63e83 100644 --- a/src/main.c +++ b/src/main.c @@ -27,6 +27,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/idf_additions.h" #include "freertos/queue.h" +#include "freertos/semphr.h" #include "freertos/task.h" #include "portal_page.h" #include "router_config.h" @@ -63,6 +64,13 @@ #define SINGMUX_CONTROL_BATCH_MAX 8 #define SINGMUX_TCP_RX_QUEUE_DEPTH 24 #define SINGMUX_TCP_DATA_MAX 2048 +/* A single forwarded stream may process this much before yielding. The + * shared throttle below + * holds all streams that reach the boundary while the + * current owner sleeps, so many active + * browser connections cannot round-robin + * forever and starve both FreeRTOS idle tasks. */ +#define SINGMUX_RELAY_YIELD_BYTES (8 * 1024) #define UDP_MANAGER_BATCH_MAX 8 #define OTA_UPLOAD_BUFFER_SIZE 1024 #define BANDWIDTH_HISTORY_SECONDS 30 @@ -220,6 +228,7 @@ static bool s_singmux_vless_response_pending; * for the owning relay to dequeue an item before retrying. */ static bool s_singmux_rx_blocked; static TaskHandle_t s_singmux_manager_task; +static SemaphoreHandle_t s_singmux_relay_throttle; static singmux_tcp_stream_t s_singmux_tcp_streams[SINGMUX_TCP_STREAM_MAX]; static singmux_stats_t s_singmux_stats; static uint32_t s_upload_bps; @@ -1895,6 +1904,30 @@ static bool singmux_tcp_close(int slot, uint32_t stream_id) return xTaskNotifyWait(0, UINT32_MAX, &answer, pdMS_TO_TICKS(1000)) == pdTRUE && answer; } +/* Do not use taskYIELD() here: another ready priority-5 relay would simply + * run next and can + * still keep the idle task from feeding its watchdog. By + * retaining this mutex for one tick, + * every relay that has exhausted its + * bounded batch is blocked together, leaving an actual idle + * opportunity. */ +static void singmux_relay_scheduler_pause(void) +{ + if (s_singmux_relay_throttle && + xSemaphoreTake(s_singmux_relay_throttle, portMAX_DELAY) == pdTRUE) + { + s_singmux_stats.scheduler_yields++; + vTaskDelay(1); + xSemaphoreGive(s_singmux_relay_throttle); + return; + } + /* The TCP listener can accept a connection just before the persistent + * manager creates + * the throttle. It must still yield in that small + * startup window. */ + vTaskDelay(1); +} + static void relay_singmux_tcp_stream(int client, int slot) { if (slot < 0 || slot >= SINGMUX_TCP_STREAM_MAX) @@ -1909,7 +1942,8 @@ static void relay_singmux_tcp_stream(int client, int slot) { return; } - bool detached = false; + bool detached = false; + size_t forwarded_since_yield = 0; for (;;) { fd_set reads; @@ -1931,6 +1965,7 @@ static void relay_singmux_tcp_stream(int client, int slot) { break; } + forwarded_since_yield += (size_t)count; } singmux_tcp_data_t *item = NULL; while (xQueueReceive(receive_queue, &item, 0) == pdTRUE) @@ -1945,15 +1980,27 @@ static void relay_singmux_tcp_stream(int client, int slot) goto done; } bool ok = socket_send_all(client, item->data, item->length); + forwarded_since_yield += item->length; free(item); if (!ok) { goto close_stream; } + if (forwarded_since_yield >= SINGMUX_RELAY_YIELD_BYTES) + { + forwarded_since_yield = 0; + singmux_relay_scheduler_pause(); + } + } + if (forwarded_since_yield >= SINGMUX_RELAY_YIELD_BYTES) + { + forwarded_since_yield = 0; + singmux_relay_scheduler_pause(); } /* A full queue can happen when this AP client cannot accept data - * quickly enough. It must close only this stream, never the shared - * session carrying unrelated browser connections. */ + * quickly enough. + * It must close only this stream, never the shared session carrying unrelated browser + * connections. */ if (s_singmux_tcp_streams[slot].peer_closed && uxQueueMessagesWaiting(receive_queue) == 0) { detached = singmux_tcp_close(slot, stream_id); @@ -2853,14 +2900,17 @@ static void transparent_udp_manager_task(void *arg) * stopped accepting data. */ smux_receive_burst = 0; ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(20)); + /* Keep a busy downstream relay from waking this same-priority + * manager + * repeatedly without giving either idle task a turn. */ + singmux_relay_scheduler_pause(); } else if (smux_receive_pending) { if (++smux_receive_burst >= 4) { smux_receive_burst = 0; - s_singmux_stats.scheduler_yields++; - vTaskDelay(1); + singmux_relay_scheduler_pause(); } } else @@ -2883,9 +2933,10 @@ static void transparent_udp_server_task(void *arg) ESP_LOGE(TAG, "transparent UDP relay listener failed"); vTaskDelete(NULL); } - s_udp_manager_queue = xQueueCreate(UDP_MANAGER_QUEUE_DEPTH, sizeof(udp_ingress_t)); - s_singmux_control_queue = xQueueCreate(SINGMUX_CONTROL_QUEUE_DEPTH, sizeof(singmux_control_t)); - if (!s_udp_manager_queue || !s_singmux_control_queue) + s_udp_manager_queue = xQueueCreate(UDP_MANAGER_QUEUE_DEPTH, sizeof(udp_ingress_t)); + s_singmux_control_queue = xQueueCreate(SINGMUX_CONTROL_QUEUE_DEPTH, sizeof(singmux_control_t)); + s_singmux_relay_throttle = xSemaphoreCreateMutex(); + if (!s_udp_manager_queue || !s_singmux_control_queue || !s_singmux_relay_throttle) { ESP_LOGE(TAG, "persistent XUDP/smux manager queue allocation failed"); vTaskDelete(NULL); diff --git a/src/portal_page.c b/src/portal_page.c index 61a5fc2..c10c347 100644 --- a/src/portal_page.c +++ b/src/portal_page.c @@ -9,13 +9,18 @@ const char *portal_page_html(void) "1rem;color:#17212b}h1{color:#0b6e4f}fieldset{border:1px solid " "#ccd5df;border-radius:8px;margin:1rem 0;padding:1rem}label{display:block;margin:.65rem " "0}.hint{color:#59636e;font-size:.9rem}input,select{box-sizing:border-box;width:100%;" - "padding:.6rem;margin-top:.2rem}.check{display:flex;align-items:center;gap:.4rem}.check input{width:auto;" + "padding:.6rem;margin-top:.2rem}.check{display:flex;align-items:center;gap:.4rem}.check " + "input{width:auto;" "padding:0;margin:0}button{padding:.65rem " "1rem;background:#0b6e4f;color:white;border:0;border-radius:5px;font-weight:600}#status{" - "margin-top:1rem}.dashboard{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px," + "margin-top:1rem}.dashboard{display:grid;grid-template-columns:repeat(auto-fit,minmax(" + "200px," "1fr));gap:.7rem;margin:1rem 0}.status-card{border:1px solid #ccd5df;border-radius:8px;" - "padding:.8rem;background:#f8fafc}.status-card h2{font-size:.9rem;margin:0 0 .45rem;color:#0b6e4f}.status-card " - "b,.status-card span{display:block;line-height:1.5}.status-card span{color:#59636e;font-size:.88rem}.metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px," + "padding:.8rem;background:#f8fafc}.status-card h2{font-size:.9rem;margin:0 0 " + ".45rem;color:#0b6e4f}.status-card " + "b,.status-card span{display:block;line-height:1.5}.status-card " + "span{color:#59636e;font-size:.88rem}.metrics{display:grid;grid-template-columns:repeat(" + "auto-fit,minmax(220px," "1fr));" "gap:1rem}.chart{border:1px solid #ccd5df;border-radius:8px;padding:.65rem}.chart h3{" "margin:0 0 .4rem;font-size:1rem}.chart " @@ -39,14 +44,19 @@ const char *portal_page_html(void) "type=password autocomplete=new-password minlength=12 maxlength=64 required>" "" "
Upstream Wi-Fi" - "" - "

Saving the same SSID again moves it to the top of the saved upstream list. The latest 10 upstream profiles are kept.

" + "id=password type=password maxlength=64 placeholder='Leave blank to keep the saved " + "password for this SSID'>" + "" + "

Saving the same SSID again moves it to the top of the saved upstream list. " + "The latest 10 upstream profiles are kept.

" "
VLESS " + "id=save_upstream>Save upstream Wi-Fi
VLESS " "(TCP)

Paste " "a plain VLESS TCP URI, or fill in the fields below.

Enable only when the VLESS " "server has sing-box multiplexing enabled. It shares one VLESS TCP session for transparent " "TCP streams and persistent UDP associations; disable it for servers without sing-box " @@ -66,62 +77,159 @@ const char *portal_page_html(void) "the ESP image, switches to it, then reboots. Do not upload the full 0x0 web-" "flash image here.

" - "

Routed payload bandwidth

" + "

Routed payload " + "bandwidth

" "" "

Collecting samples...

" "Tunnel pressure

" - "

Collecting samples...

Chip temperature

Collecting samples...

CPU load by core

Collecting samples...

Collecting samples...

Chip temperature

Collecting " + "samples...

CPU load by core

Collecting " + "samples...

Firmware: loading...

" ""; + "textContent='Router status refresh " + "failed').finally(()=>refreshInProgress=false)},1000);"; return page; }