Skip to content

Отчет по багфиксам и оптимизациям: 27-31 июля 2026

Резюме

За неделю были закрыты четыре взаимосвязанные инженерные задачи:

  1. Стабилизирован WebRTC media lifecycle: восстановление звука, публикация микрофона, статичный screen share, signaling и совместимость Firefox.
  2. Убраны основные источники неограниченного роста памяти и фоновых потоков на сервере.
  3. Чаты переведены с полной загрузки истории на постраничную модель с локальным кэшем, outbox и поиском.
  4. 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 libdatachannel warning/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 был оптимизирован в два этапа:

  1. Убраны повторный RTP parse/serialize, per-recipient allocation и удержание mutex во время fan-out.
  2. На Linux N x sendto заменены на sendmmsg batches до 64 получателей.

CPU scalar и batch

Контрольный 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

Доставка Translator

При штатной нагрузке обе реализации доставили 100% принятых packets. На границе насыщения batch path сохранил 98,7-100% входного потока, scalar принимал 89,2-93,9%.

Системные вызовы Translator

Полная методика, 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. Следующие шаги

  1. Оформить минимальный upstream PR в paullouisageneau/libdatachannel: runtime probe bundled libSRTP GCM support.
  2. Добавить client telemetry percentiles для offer -> ICE -> DTLS -> first RTP, чтобы заменить субъективную оценку старта звука численными p50/p95.
  3. Наблюдать production counters slow-consumer disconnect и размер WS queue.
  4. Сравнить production CPU/packet loss Translator до и после sendmmsg.
  5. Рассматривать 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