# Perf Этап 5 — структурный send-путь ## Контекст (почему так, а не «всё подряд») Профиль (см. `perf-network-send-path`) показал: под нагрузкой map упирается в **сетевое ядро** — syscall `send()` + блокировка qdisc виртио, а не в userspace-копии. Для TCP **нельзя** одним syscall отправить разным сокетам (это не умеет ядро без io_uring), а per-fd коалесинг малых пакетов в один send() **уже сделан** (`socket_async_send`+`socket_send_coalesce_ms`). Поэтому из трёх задуманных под-частей этапа реально полезна и безопасна одна — 5a; 5b/5c отложены (ниже — почему). ## Сделано: 5a — пул переиспользования буферов воркера `send_worker.c` на каждую передачу пакета делал `malloc(chunk)` и `free()` после отправки — при сотнях тысяч пакетов/сек это malloc/free на **каждый** пакет. Добавлен небольшой пул переиспользуемых буферов фиксированного размера (`SW_SLAB = 4096`, кап `SW_FREELIST_MAX = 512` ≈ 2 МБ простаивающих): - мелкие пакеты (≤ slab) берутся из пула и возвращаются в него — без обращения к аллокатору; - крупные (> slab, редкие — большие инвентари и т.п.) идут прямым `malloc`/`free`, не пулятся; - у пула **свой** мьютекс (`g_pool_mtx`), он не расширяет contention основного `g_mtx`; пул трогают оба потока (producer `sendworker_send` и воркер), поэтому блокировка обязательна. **Поведенчески-идентично:** меняется только источник памяти под буфер, а не данные/порядок/тайминг. При выключенном пуле — ровно прежний путь (`malloc` точного размера / `free`). ### Флаг `conf/battle/misc.conf`: ``` socket_send_pool: 1 // 1 (деф.) = переиспользовать буферы воркера; 0 = malloc/free на каждый пакет ``` Требует `socket_async_send: 1`. (battle_config; применяется при старте воркера.) ### Тест `src/common/test_send_worker.c` расширен тестом 7 (`t_pool_mix`): смесь мелких (пул) и крупных (>slab, байпас) чанков на многих fd + A/B-переключение режима пула между вызовами (буфер, выделенный в одном режиме, освобождается в другом — проверка паритета). Прогон: ``` gcc -O1 -g -fsanitize=address -I. -o /tmp/tsw_asan test_send_worker.c send_worker.c -lpthread && /tmp/tsw_asan gcc -O1 -g -fsanitize=thread -I. -o /tmp/tsw_tsan test_send_worker.c send_worker.c -lpthread && /tmp/tsw_tsan ``` **ASan: ALL PASSED (нет утечек/UAF). TSan: ALL PASSED (нет гонок).** Полная сборка + локальный буст: воркер с пулом стартует, `socket_send_pool` парсится, краша нет. ## Отложено (с обоснованием) - **5b — `writev`-разброс вместо merge-копии:** воркер при коалесинге сливает чанки в один буфер (одна `memcpy`) и шлёт одним `send()`. `writev` убрал бы только эту merge-копию — но она уже **вне игрового потока** (в воркере), а корректная обработка частичной записи `writev` на EAGAIN заметно усложняет код. Выигрыш маржинальный; не делаем сейчас. - **5c — refcount общего broadcast-буфера (1 копия + K refbump вместо 2K копий):** бьёт по userspace-`memcpy`, а узкое место — **ядро** (syscall/qdisc), не копии. Хуже того: безопасно внедрить нельзя без переработки модели `wdata`/flush — broadcast через общий буфер ушёл бы в очередь воркера **раньше** ещё не сброшенного `wdata` того же клиента (SELF-пакеты в той же итерации) → **переупорядочивание пакетов** → массовые дисконнекты в WoE. Высокий риск ради не-узкого места. Не делаем. ## Дальше по сети (за рамками этого этапа, на будущее) - **`SO_SNDBUF`** (`socket_sndbuf_size`, уже реализован, деф. 0=ядро): на нагруженном сервере стоит попробовать `131072–262144` — больше буфер ⇒ реже EAGAIN, который рвёт коалесинг. **Рекомендация тестировщикам:** включить и сравнить. - **io_uring** — единственный способ реально срезать syscall/qdisc-оверхед (батч-сабмишн отправок, меньше переключений в ядро). Крупная архитектурная переделка (ядро 6.1 на боксе это тянет); кандидат на отдельный большой этап, если профиль тестировщиков подтвердит, что сеть-ядро — доминирующий расход при 300 онлайн. - **Ops-сторона:** многоочередь virtio-net + RPS/XPS, apparmor unconfined — вне кода. ## Чек-лист корректности (тестировщикам) - [ ] Игра идёт нормально с `socket_send_pool: 1` (движение/бой/скиллы/чат/варпы — без потерь пакетов и дисконнектов). - [ ] `0` vs `1` — идентичное поведение (разница только в нагрузке на аллокатор). - [ ] Длительная сессия / WoE-нагрузка: память стабильна (пул капится 512 буферами; нет роста RSS от пула). - [ ] Массовые реконнекты/burst-трафик — штатно (release/reset воркера корректны). - [ ] (Опц.) Профиль `UA_PERF=1`: при `1` доля `malloc`/`free` в send-пути ниже, чем при `0`. ## Откат `socket_send_pool: 0` (+ рестарт map; флаг применяется на старте воркера).