Отчет по багфиксам и оптимизациям: 27-31 июля 2026
Резюме
За неделю были закрыты четыре взаимосвязанные инженерные задачи:
- Стабилизирован WebRTC media lifecycle: восстановление звука, публикация микрофона, статичный screen share, signaling и совместимость Firefox.
- Убраны основные источники неограниченного роста памяти и фоновых потоков на сервере.
- Чаты переведены с полной загрузки истории на постраничную модель с локальным кэшем, outbox и поиском.
- RTP Translator получил измеримый Linux fast path на
sendmmsgи воспроизводимый benchmark.
Итог production-проверки
После выкладки исправлений Firefox начал передавать и принимать звук и видео. Наблюдавшаяся ранее задержка появления звука в 3-6 секунд исчезла; Chrome также стал субъективно подключать звук быстрее. Это эксплуатационное наблюдение, а не синтетический benchmark.
Объем изменений
Отчет охватывает изменения ветки за 27-31 июля:
| Показатель | Значение |
|---|---|
| Коммиты | 28 |
| Измененные файлы | 63 |
| Добавлено строк | 6 166 |
| Удалено строк | 448 |
| Основные области | WebRTC, Server, Translator, Chats |
Количество строк само по себе не является метрикой качества. Оно приведено только для оценки масштаба: значительная часть объема приходится на regression tests, benchmark и документацию.
1. WebRTC: быстрый и устойчивый media startup
Наблюдавшиеся симптомы
В течение недели разбирались несколько внешне разных проблем:
- Firefox подключал ICE, но не передавал микрофон и камеру;
- удаленный звук мог затухнуть в работающей конференции и восстановиться только после повторного входа;
- после перезапуска сервера локально микрофон оставался зеленым, а у остальных участников был выключен;
- изменение browser bandwidth estimate переанонсировало исправные media devices и на время глушило звук;
- статичная демонстрация экрана считалась зависшей и периодически пересоздавалась;
- PWA могла получить системный mute микрофона в background и не восстановить capture после возврата;
- несколько параллельных recovery callbacks создавали гонки между cleanup старой конференции и новым join.
Главная методологическая правка: transport state, capture state и наличие акустического сигнала больше не смешиваются.
| Сигнал | Что он означает | Чего он не означает |
|---|---|---|
ICE connected/completed |
Сетевой путь до peer найден | DTLS/SRTP и RTP уже работают |
| Outbound RTP progress | Sender отправляет RTP/DTX | В микрофоне обязательно есть речь |
Track muted |
Browser временно приостановил source | Пользователь выключил микрофон |
| Нулевой audio level | Тишина или DTX | Capture завис |
| Статичный screen frame | Картинка не меняется | Публикация экрана потеряна |
Recovery без лишнего переанонса
Исправления media recovery:
- browser network estimate теперь меняет bitrate существующего sender без
disconnect_deviceи создания нового device; - Opus DTX во время тишины не считается остановкой микрофона;
- статичная демонстрация экрана не считается outbound stall;
- forced recovery микрофона выполняется только при подтвержденной остановке capture frames или длительном browser mute на видимой странице;
- системный mute hidden PWA не запускает разрушительный restart до возврата приложения;
- локальные media operations сериализованы, а новый join ждет завершения cleanup предыдущей конференции;
- aggregate failure нескольких независимых remote audio subscriptions может выполнить ограниченный hard rejoin, сохранив intent микрофона и камеры;
- recovery имеет cooldown и верхний предел попыток.
flowchart TD
H["Media health event"] --> K{"Какой сигнал?"}
K -->|ICE failed/disconnected| R["Scoped WebRTC restart"]
K -->|Capture frames stopped| M["Mic capture recovery"]
K -->|Browser muted + visible| M
K -->|Audio level = 0 / DTX| OK["Сессия остается активной"]
K -->|Static screen / no new frame| OK
K -->|Bandwidth estimate changed| B["Update sender bitrate in place"]
R --> Q["Общая media recovery queue"]
M --> Q
Q --> G["Generation check + bounded retry"]
Почему Firefox не работал
Свежая диагностика разделила ошибку на два этапа.
Signaling
Клиент мог отправить trickle ICE candidate раньше SDP offer. Кроме того, Firefox создавал candidate для RTCP component 2, хотя соединение использует rtcp-mux и server ICE path обслуживает RTP component 1.
Исправлено:
- local candidates буферизуются до отправки offer;
- candidate component 2 не отправляется для publish, audio subscribe и video subscribe;
- порядок закреплен шестью regression tests.
DTLS/SRTP
После signaling-исправления серверный лог показал точную последовательность:
ICE connected/completed
DTLS recv: SRTP profile is not supported
DTLS handshake failed
Linux-сборка использует bundled libsrtp без OpenSSL crypto backend. Такая библиотека не поддерживает AES-GCM, но libdatachannel для bundled-варианта безусловно возвращала IsGcmSupported() == true. Firefox выбирал предложенный GCM profile, после чего DTLS завершался ошибкой.
Исправление внесено в vendored libdatachannel:
- поддержка GCM проверяется фактическим вызовом API
libsrtpдля RTP и RTCP; - если GCM недоступен, SDP/DTLS предлагает обязательный
SRTP_AES128_CM_SHA1_80; - ошибки post-handshake теперь содержат направление, profile и status;
- low-level
libdatachannelwarning/error направлены в журнал VideoGrace; - отдельно логируется ICE state, а не только общий peer state.
Проверка той же конфигурации, что используется в production:
gcm_rtp=2 gcm_rtcp=2 aes_cm=0
2 означает, что GCM profile не поддержан текущей libsrtp; 0 подтверждает поддержку AES-CM. После пересборки именно libdatachannel, перелинковки сервера и выкладки Firefox заработал.
sequenceDiagram
participant F as Firefox
participant S as VideoGrace signaling
participant G as libdatachannel Gateway
participant L as bundled libsrtp
F->>S: SDP offer
F->>S: ICE component 1
S->>G: offer + candidate
G->>L: probe AES-GCM RTP/RTCP
L-->>G: not supported
G-->>F: answer with AES128_CM_SHA1_80
F->>G: ICE connected
F->>G: DTLS handshake
G-->>F: SRTP ready on first successful path
Почему быстрее стало и в Chrome
SRTP bug был специфичнее Firefox, но в той же серии исправлений изменился общий browser signaling/recovery path:
- offer всегда предшествует trickle candidates;
- лишний RTCP component 2 не попадает в server ICE;
- bitrate update не уничтожает исправную публикацию;
- параллельные cleanup/join операции сериализованы;
- ложные stalls тишины и статичного экрана не создают новые peer connections.
Поэтому ускорение Chrome объясняется не GCM fallback как таковым, а устранением лишних restart/reannounce и детерминированным signaling. Для строгой количественной оценки нужно собирать клиентские интервалы offer_sent, ICE connected, DTLS ready, first RTP.
2. Media UX и управление конференцией
Вместе с transport fixes были добавлены пользовательские инварианты:
- музыкальный режим: 48 кГц, stereo, отключенные echo cancellation/noise suppression/AGC, Opus до 196 кбит/с;
- music mode, speaker mute и screen-share intent сохраняются в
sessionStorageтекущей вкладки; - модератор может управлять демонстрацией экрана и музыкальным режимом участника;
- защищенный browser API показа экрана все равно требует подтверждения самим участником;
- выход founder больше не завершает конференцию, если остается хотя бы один подключенный модератор;
- статусы микрофона, камеры и screen share синхронизируются по реальному device/media lifecycle.
Использование sessionStorage, а не localStorage, принципиально: две вкладки могут находиться в разных конференциях и не перезаписывают media intent друг друга.
3. Устойчивость сервера
Ограниченная WebSocket write queue
Ранее большой чат, массовая доставка истории или медленный клиент могли создавать очередь serialized messages без жесткого byte limit. Зарезервированные Asio handlers сами удерживали строки и увеличивали память еще до помещения в transport queue.
Новая модель резервирует место до net::post:
| Ограничение | Значение |
|---|---|
| Pending messages | 256 |
| Pending bytes | 16 MiB |
| Реакция на превышение | Disconnect slow consumer |
Счетчики уменьшаются при успешной отправке, отмене posted handler и очистке после write error. Это ограничивает память одной WebSocket-сессии и не позволяет одному клиенту вызвать каскадный OOM всего процесса.
flowchart LR
P["Producer"] --> R{"reserve item + bytes"}
R -->|within 256 / 16 MiB| A["Asio executor"]
A --> W["WebSocket write"]
W --> C["release reservation"]
R -->|limit exceeded| D["Abort slow consumer"]
Удаление неограниченных фоновых работ
Server-side WebRTC/CAN lifecycle также стал ограниченным:
- detached timeout thread на каждый RTC offer заменен одним deadline worker;
- deadlines хранятся в priority queue;
- callbacks используют weak shared state и не обращаются к уничтоженному Gateway;
- destructor останавливает worker, снимает handlers и завершает активные endpoints;
- пустые endpoint lists удаляются из session index.
Дополнительно сокращены копирования HTTP/WS payload и улучшено безопасное завершение session callbacks.
4. Чаты: история без перегрузки клиента и сервера
До
При подключении клиент мог получить сотни или тысячи сообщений каждого чата. Это одновременно:
- создавало большой JSON payload;
- копировало историю при сериализации;
- загружало древние картинки и вложения;
- помещало все сообщения в React state и DOM;
- замедляло ввод текста из-за повторных render/reconciliation;
- увеличивало WS queue и риск OOM.
Дополнительно сервер собирал результат вставками в начало массива, что давало квадратичную стоимость на большой истории.
После
Текущая модель разделяет summary, активное окно и полную историю:
flowchart LR
DB[("history.db")]
S["Latest meaningful message<br/>per chat"]
P["Cursor page<br/>20 messages in UI<br/>server limit <= 100"]
I[("IndexedDB")]
R["React state / DOM<br/>visible window"]
DB --> S --> R
DB --> P --> I
I --> R
R -->|"scroll up"| P
Ключевые изменения:
- при общей синхронизации загружается последнее содержательное сообщение каждого диалога;
- открытый чат берет последние 20 сообщений;
- более старая история загружается по стабильному курсору
(dt, guid); - сервер ограничивает страницу диапазоном
1..100; - newest-first result разворачивается один раз через
reverse, без O(n²) вставок в начало; history_page.total_countотделяет полный размер чата от числа сообщений в памяти;- древние attachments не попадают в DOM до прокрутки;
- IndexedDB остается локальным кэшем;
- отдельный outbox повторяет после reconnect только неподтвержденные исходящие сообщения.
Поиск и UX истории
Поиск работает по локальной IndexedDB конкретного чата:
- результаты показываются справа, не скрывая переписку;
- клик догружает нужную страницу, прокручивает к исходному сообщению и подсвечивает слово;
- полный счетчик сообщений приходит с сервера;
- при уходе далеко вверх появляется кнопка возврата вниз;
- на мобильных устройствах scroll chain списка сообщений изолирован через
overscroll-behavior, поэтому жест истории не превращается в browser pull-to-refresh; - founder конференции может удалить любое сообщение ее чата, причем сервер проверяет права по записи в БД;
- preview и push больше не показывают сырой Markdown вложений/цитат;
- mute контакта или конференции хранится на сервере и подавляет push, но не сообщения и unread counters.
Подробный protocol contract находится в разделе Чаты и история сообщений.
5. RTP Translator: измеренная оптимизация hot path
Translator был оптимизирован в два этапа:
- Убраны повторный RTP parse/serialize, per-recipient allocation и удержание mutex во время fan-out.
- На Linux
N x sendtoзаменены наsendmmsgbatches до 64 получателей.
Контрольный A/B benchmark на stage1.videograce.ru:
| Сценарий | Результат |
|---|---|
| 16 receivers, 10 000 RTP/s | -4,5% process CPU |
| 64 receivers, 5 000 RTP/s | -26,1% CPU/fan-out packet |
| Burst 50 000 input RTP | +9,0% accepted input |
| 16 receivers syscall profile | -88,2% send syscall |
При штатной нагрузке обе реализации доставили 100% принятых packets. На границе насыщения batch path сохранил 98,7-100% входного потока, scalar принимал 89,2-93,9%.
Полная методика, raw numbers и команды воспроизведения находятся в отчете Производительность RTP Translator.
6. Regression coverage и диагностика
Ключевые затронутые test suites сейчас содержат 51 focused scenario:
| Suite | Сценарии |
|---|---|
| Conference lifecycle | 10 |
| Media recovery | 22 |
| WebRTC signaling order | 6 |
| Publish health/music | 4 |
| Message storage/outbox/search | 3 |
| Message preview | 6 |
Особенно важные закрепленные случаи:
- ICE candidate не отправляется до offer;
- Firefox RTCP component 2 игнорируется во всех трех session types;
- network bitrate update не запускает media recovery;
- silence/DTX и static screen не считаются stalled publication;
- hidden PWA не перезапускает system-muted microphone;
- stale cleanup завершается до повторного join;
- aggregate audio recovery требует разные remote sources и ограничено cooldown;
- offline outbox удаляет pending marker только после server acknowledgement;
- chat search ограничен выбранным диалогом.
Runtime diagnostics теперь позволяют различать:
- signaling order;
- ICE gathering и ICE connection;
- общий PeerConnection state;
- DTLS/libdatachannel error;
- SRTP profile/status;
- RTP health и capture health;
- причину recovery/reannounce.
Финальная проверка ветки:
Test Files 10 passed (10)
Tests 56 passed (56)
MkDocs strict build passed
SVG XML validation passed
Linux SRTP gcm_rtp=2 gcm_rtcp=2 aes_cm=0
7. Статус выкладки
| Среда | Статус |
|---|---|
stage1.videograce.ru |
libdatachannel пересобрана, сервер перелинкован и перезапущен |
join.videograce.ru |
Production обновлен, Firefox media подтвержден |
| Chrome | Полевое наблюдение: звук начинает работать быстрее |
| Translator batch | Включен по умолчанию на Linux, scalar fallback на macOS/Windows |
8. Следующие шаги
- Оформить минимальный upstream PR в
paullouisageneau/libdatachannel: runtime probe bundled libSRTP GCM support. - Добавить client telemetry percentiles для
offer -> ICE -> DTLS -> first RTP, чтобы заменить субъективную оценку старта звука численными p50/p95. - Наблюдать production counters slow-consumer disconnect и размер WS queue.
- Сравнить production CPU/packet loss Translator до и после
sendmmsg. - Рассматривать
recvmmsgтолько после подтвержденногоSO_RXQ_OVFLили профиля bottleneck на входномrecvfrom.
Связанные изменения
| Область | Коммиты |
|---|---|
| Screen/audio recovery | d2b884464, 1286a201a |
| Music mode и bitrate update | 7d187dd19, 9fc44434e |
| Conference/media lifecycle | 4da2943df, aed594706, 48744e85e |
| Server memory и chat history | 639a050cf, cb965c0ba, 23655ae0f |
| Server bounded work | e84b7a4b6 |
| Translator benchmark/batch | 06894fcf2, 52a8e8d29, 47ff5ed26 |
| Firefox signaling и SRTP | f3f5618c0, 3e06cd56e, 2f3ba6039 |