59 changed files with 4582 additions and 1412 deletions
@ -0,0 +1,354 @@
|
||||
# Linux: диагностика и восстановление аудио |
||||
|
||||
Проверка 2026-10-01: desktop chatgui, miniaudio/PulseAudio, общий голосовой стек, |
||||
звонки, рация и запись сообщений. Исправления обработки PCM, владения устройствами |
||||
и автоматическое восстановление реализованы. Ниже сохранены исходные результаты |
||||
аудита; раздел «Реализация» описывает действующие контракты и проверки. |
||||
|
||||
## Вывод |
||||
|
||||
До исправлений тракт был некорректен при сбоях. Самое существенное: воспроизведение |
||||
звонка зависит от наличия микрофонных кадров в duplex-ring. Его опустошение |
||||
останавливает вызов пользовательского callback, оставляя хвост выходного буфера |
||||
незаполненным. Восстановление jitter-буфера не исправляет остановившееся устройство. |
||||
Отдельного управляющего механизма восстановления устройств не было. |
||||
|
||||
В реализации восстановлены три гарантии: каждый запрос output полностью заполнен; обработка |
||||
одного callback всегда ограничена; остановка/пересоздание устройства выполняются |
||||
вне аудиопотока и не уничтожают сетевую сессию звонка. |
||||
|
||||
## Что установлено по работающим клиентам |
||||
|
||||
В локальном старом логе звонка `132e4a9698f47bdf` с Samsung SM_A525F наблюдались |
||||
паузы callback примерно 0,5–2,6 секунды при зарегистрированной обработке 0–2 мс. |
||||
Входящий буфер desktop достигал 15 секунд, росли потери, включалось ускорение 160%. |
||||
На Samsung захват продолжал давать около 50 кадров/с, playback регистрировал |
||||
underrun и тоны разрыва. RTT порядка 1–4 мс не объясняет эти паузы сам по себе. |
||||
|
||||
Это признаки проблемы обслуживания локального аудио, но ещё не доказательство |
||||
конкретной причины зависания PulseAudio. Старое измерение исключало время |
||||
синхронных DEBUG-вызовов между концом обработки и обновлением timestamp. |
||||
Новая диагностика охватывает весь callback, включая эти логи. |
||||
Работающий GUI не перезапускался, поэтому новый инструмент ещё не записал этот |
||||
сбой на реальном звонке. Лог Samsung сохранён в `/tmp/sm-audio-current.log`. |
||||
|
||||
## Найденные дефекты и ограничения до исправлений |
||||
|
||||
| Место | Поведение и последствия | Необходимая доработка | |
||||
| --- | --- | --- | |
||||
| `lib/miniaudio.h`, `ma_device__handle_duplex_callback_playback()` | При пустом capture-ring `acquire_read` возвращает success с нулём кадров. Выполняется `break`, output остаётся частично незаполненным, call callback не вызывается. PulseAudio затем отправляет весь mapped buffer. | Обеспечить полный output и продолжение playback при отсутствии capture. Нельзя делать commit_read для синтетических кадров. | |
||||
| `ma_device_on_write__pulse()` | Успешная запись с нулём обработанных кадров не уменьшает остаток цикла. Возможно бесконечное вращение с блокированием того же PA mainloop, включая capture. | Немедленно выходить при отсутствии продвижения, возвращать управление mainloop. Логировать состояние и запрос, не пытаться записывать до успеха в этом callback. | |
||||
| `ma_device_on_read__pulse()` | PA hole отбрасывается без подачи соответствующего количества нулевых кадров. Из capture timeline исчезает интервал, duplex-ring может опустеть. | Сохранять длительность hole тишиной либо явно обозначать discontinuity независимому capture-тракту. | |
||||
| `lib/speex_aec.c`, `speex_aec_feed_playback()` | Обрабатывает только остаток одного 960-sample кадра, отбрасывая хвост входного блока. Контракт разрешает произвольный count. | Циклом потреблять весь count, складывать все полные кадры, сохранять остаток. | |
||||
| `CallAudioEngine::setPlaybackDeviceIndex()` | После неудачного пересоздания `m_active` остаётся true при отсутствии устройства; `start` того же звонка возвращает успех. | Разделить желание продолжать звонок и работоспособность устройства. Ошибка запуска переводит устройство в recovering. | |
||||
| `RadioAudioEngine::teardownDevice()` | Если playback init успешен, а start нет, uninit пропускается: он зависит от флага started. Объект удаляется вместе с незавершёнными ресурсами. | Разделить initialized и started; каждый успешный init обязательно завершать uninit. | |
||||
| `MainWindow::~MainWindow()` / `SoundManager::shutdown()` | Общий context освобождается вместе с engine, но звонок и рация здесь явно не останавливаются. Их singleton-объекты могут оставлять живые устройства. `m_context` также не обнуляется. | Сначала остановить все зависимые устройства и дождаться callbacks, затем освобождать общий context. | |
||||
| `AudioRecorder` | Callback выбирает глобальный `g_recorder`, игнорируя `pUserData`. Второй recorder, в том числе тест микрофона, заменяет получателя. Флаги/уровень/вектор waveform используются из GUI и аудиопотока без согласованной синхронизации. | Использовать `pUserData`; передавать уровни атомарно, waveform — безопасным snapshot; буфер записи должен иметь однозначного владельца. | |
||||
|
||||
До исправлений первые два дефекта воспроизводились стендом без аппаратуры: |
||||
duplex оставлял sentinel в output при starvation, а PA-write без продвижения |
||||
требовал внешней остановки. AEC из 1200 + 720 samples сохранял один кадр вместо |
||||
двух. Теперь эти проверки требуют полного output, самостоятельного возврата |
||||
из callback и двух сохранённых AEC-кадров; результаты приведены ниже. |
||||
|
||||
Capture-AEC также обрабатывает не более одного кадра за вызов. В звонке его |
||||
вызывают ровно с 960 samples, поэтому текущий call feed этот случай не нарушает. |
||||
При расширении API нельзя объявлять произвольный count без согласованного размера |
||||
выходного буфера. `speex_aec_reset()` уже существует: новый API для сброса самого |
||||
AEC не нужен, но нужен согласованный сброс всего локального voice-тракта. |
||||
|
||||
## Чистота реализации |
||||
|
||||
Этот раздел фиксирует исходный аудит; изменения владения и ограничение работы |
||||
callback описаны в разделе «Реализация». |
||||
|
||||
Нормальная структура уже есть: GUI владеет устройствами, сервисный слой кодирует |
||||
Opus и обрабатывает jitter, сетевой поток доставляет пакеты. Нет необходимости |
||||
переписывать весь стек или заводить новый backend. |
||||
|
||||
Однако границы владения не завершены: context фактически принадлежит engine |
||||
звуковых уведомлений, а используется независимыми устройствами звонка/рации. |
||||
Флаги active/started/initialized местами смешивают три разных состояния. |
||||
Выбор input применяется к recorder, звонок использует default input, рация — |
||||
default устройства. Output звонка и SoundManager тоже выбираются отдельно. |
||||
Индекс перечисления не является устойчивой идентичностью устройства после hotplug. |
||||
В radio capture fallback `useDefault` фактически повторяет ту же попытку: |
||||
`pDeviceID` и так NULL в обеих ветках. Callbacks рации запускаются раньше |
||||
подготовки capture-буфера и установки VAD; порядок запуска нужно привести к |
||||
«сначала состояние и буферы, затем устройства», чтобы исключить гонки при старте. |
||||
|
||||
Callback не является строго realtime-safe. Capture удерживает `g_mtx` во время |
||||
AEC/AGC/Opus; отправка выделяет память и вызывает `uasync_post`. Playback вызывает |
||||
SoundTouch и код с возможным расширением контейнеров, mutex и синхронными логами. |
||||
Приём пакетов тоже может держать mutex при работе с jitter и логировании. |
||||
Плохие Opus-кадры могут последовательно выбираться из большой очереди в одном |
||||
pull: нужен предел работы на callback. Новая диагностика покажет, где это |
||||
действительно нарушает deadline. Перенос всего DSP в ещё один worker заранее |
||||
увеличит число очередей и точек синхронизации без установленной необходимости. |
||||
|
||||
AEC получает reference только звонка. Звуки отдельного SoundManager в него не |
||||
попадают; delay по умолчанию фиксирован, не вычисляется по фактической задержке |
||||
устройств. Это ограничение качества эхоподавления, а не установленная причина |
||||
текущего зависания. |
||||
|
||||
Jitter умеет повторно накопить резерв после сетевого underrun, но диапазон tempo |
||||
ограничен 1,0–1,6. Ускорение убирает избыток, а замедления для компенсации более |
||||
медленных часов отправителя нет. При устойчивом отрицательном дрейфе резерв будет |
||||
снова опустошаться. Это отдельная задача длительных звонков: после восстановления |
||||
устройств проверить наклон глубины буфера и при необходимости добавить небольшую |
||||
компенсацию дрейфа в обе стороны, не смешивая её с быстрым catch-up после паузы. |
||||
|
||||
Принудительные 480 frames задают период 10 мс при 48 кГц. Собственный PA default |
||||
miniaudio — 25 мс с комментарием о проблемах PipeWire при периодах меньше ~20 мс. |
||||
Период backend и Opus-кадр 20 мс должны оставаться независимыми. Увеличивать |
||||
период до исправления AEC нельзя: 1200-sample блоки уже воспроизводят потерю хвоста. |
||||
После исправления разумно сначала проверить default backend и измерить задержку. |
||||
|
||||
## Предлагаемое восстановление без большого усложнения |
||||
|
||||
### 1. Сначала восстановить инварианты обработки и владения |
||||
|
||||
Исправить полный output, zero-progress, AEC accumulation и uninit при неудачном |
||||
start. Перед освобождением context явно остановить recorder, звонок, рацию и |
||||
прочие его устройства. До остановки отменять recovery timer; после join можно |
||||
сбрасывать voice-ресурсы. Проверять возвраты feed/start/init и логировать причину. |
||||
|
||||
Самый небольшой вариант — оставить один duplex-device: при нехватке capture |
||||
подать в call callback нулевой input, полностью сформировать playback и не |
||||
продвигать реальный capture-ring за синтетические samples. Это чинит непосредственный |
||||
дефект, но сохраняет общий жизненный цикл input/output и синтетический TX по |
||||
playback-clock. |
||||
|
||||
Для устойчивости к исчезновению микрофона предпочтительнее два устройства |
||||
capture/playback внутри существующего `CallAudioEngine`, как уже сделано в рации. |
||||
Capture только накапливает 960 samples и вызывает feed; playback независимо |
||||
вызывает pull и всегда заполняет output. Это два handle с одним владельцем, |
||||
без нового DSP-worker или дополнительной PCM-очереди. Потребуется уточнить |
||||
потоковый контракт voice-слоя: один capture-writer и один playback-writer, |
||||
согласованный доступ к AEC, остановка обоих до release. Текущий `g_mtx` уже |
||||
защищает AEC, но остальные общие поля нужно проверить перед разделением. |
||||
|
||||
### 2. Разделить краткий underrun и неисправность устройства |
||||
|
||||
| Событие | Действие | |
||||
| --- | --- | |
||||
| Нет сетевого PCM, callback продолжает приходить | Тишина/существующий gap tone; jitter автоматически накапливает данные. Устройство не пересоздавать. | |
||||
| Один backend underrun, новые кадры принимаются | Подать следующий полностью заполненный буфер; собрать счётчик. Не сбрасывать AEC и не запускать restart на каждый xrun. | |
||||
| Hole/переполнение capture | Учесть потерянную длительность; краткую потерю пережить, длительную отметить discontinuity для AEC. | |
||||
| Поток failed/stopped или нет продвижения кадров длительное время | Перейти в recovering и пересоздать затронутое устройство вне callback. | |
||||
| Suspend/resume ОС | Учитывать suspend как отдельную причину, восстановление проверять после resume; не запускать частый restart-loop во время сна. | |
||||
| Потерян общий backend/context | Однократно остановить всех его пользователей, восстановить владельца, затем требуемые устройства. | |
||||
|
||||
Один owner timer и состояния `Stopped / Running / Recovering` достаточны. |
||||
`wanted` хранит желание пользователя продолжать; generation меняется при stop, |
||||
новом звонке и смене устройства. Каждая отложенная попытка проверяет generation |
||||
и wanted — законченный звонок никогда не возрождается. |
||||
|
||||
Callbacks только обновляют атомарные счётчики успешно обработанных кадров и |
||||
признак backend failure. Watchdog проверяет фактический прогресс отдельно по |
||||
направлениям. Тишина в PCM не означает остановку устройства. Reporter диагностической |
||||
очереди не должен становиться watchdog: его события могут теряться. |
||||
|
||||
Начальные значения для проверки: timer 200 мс, подозрение при отсутствии прогресса |
||||
500 мс после startup grace; повторные попытки через 250/500/1000 мс с потолком |
||||
2 секунды. Это предлагаемые настройки, не доказанные универсальные пределы. |
||||
Suspend и фактический согласованный backend period должны учитываться отдельно. |
||||
Нельзя обещать bounded recovery, пока init/stop/uninit сами могут зависнуть: |
||||
сначала устранить spin-loop и проверить время backend-операций. Если они реально |
||||
блокируют GUI, вынести только управление устройствами в один owner-thread, |
||||
сохранив тот же автомат состояний. |
||||
|
||||
На пересоздании сбросить capture-accumulator, AEC delay/filter и локальный PCM/ |
||||
stretch, сохранив call ID и сетевую сессию. После длительной аппаратной паузы |
||||
устаревшую речь нужно явно отбросить до актуального резерва jitter, иначе |
||||
15 секунд накопленного звука при скорости 160% разгружаются примерно 25 секунд. |
||||
Этот сброс применять только при аппаратной discontinuity; обычный сетевой jitter |
||||
не должен автоматически приводить к отбрасыванию очереди. |
||||
|
||||
Устройство хранить по backend ID либо как default, не по индексу. При его |
||||
исчезновении попробовать default и явно сообщить о смене; выбранное устройство |
||||
можно вернуть при появлении. Нужны ошибки с ID/именем/направлением и результатом |
||||
попытки. Для PA-ошибок следующий уровень диагностики — stream/context state и |
||||
`pa_context_errno`, потому что один `-1` от API не объясняет причину. |
||||
|
||||
### 3. Проверить сценарии, а не только успешный старт |
||||
|
||||
После реализации добавить проверки: capture starvation 20/500 мс и возврат; |
||||
hole; AEC блоки, пересекающие границу кадра; zero writable; ошибка записи; |
||||
исчезновение выбранного устройства и появление default; suspend/resume; |
||||
перезапуск звукового сервера; stop во время backoff; закрытие GUI при активном |
||||
звонке/рации. Проверять полный output, bounded очереди и работу callback, |
||||
возобновление прогресса, отсутствие callbacks после release и отсутствие |
||||
повторного старта после пользовательского stop. |
||||
|
||||
## Как читать добавленную диагностику |
||||
|
||||
В GUI-категориях включить `call=debug` либо задать отдельную строку в INI: |
||||
|
||||
```ini |
||||
[debug] |
||||
call=debug |
||||
``` |
||||
|
||||
Обычные сводки идут на DEBUG, аномалии — WARN/ERROR; miniaudio info перенесён |
||||
на DEBUG, подробный backend log — TRACE. Все устройства используют категорию |
||||
call, включая recorder, radio и sound-engine; role и ID различают их в строках. |
||||
Не включать глобальный trace для всего проекта при измерении аудио. |
||||
|
||||
Reporter раз в секунду печатает stage, tid, callbacks/s, requested frames/s, |
||||
максимальный интервал между входами, полное wall-time обработки и thread CPU-time, |
||||
незавершённый вход и время с последнего входа. `work_max` и `cpu_max` — отдельные |
||||
максимумы за окно, они не обязательно относятся к одному вызову. |
||||
|
||||
| Stage | Что измеряется | |
||||
| --- | --- | |
||||
| loop | Один `pa_mainloop_iterate`, включая ожидание и вложенные callbacks. Долгое время само по себе не доказывает тяжёлый DSP. | |
||||
| read / write | Полный backend read/write callback. Frames/s здесь — запрошенные, не гарантированно обработанные кадры. | |
||||
| map | `writable_size` и `begin_write`; пустой/ошибочный выход тоже закрывает измерение. | |
||||
| submit | Вызов `pa_stream_write`. | |
||||
| duplex | Перенос capture-ring в duplex-output; detail содержит глубину capture-ring. | |
||||
| callback | Полный miniaudio data callback, включая код приложения и его синхронные логи. | |
||||
| feed / pull | Capture DSP/encode/send и playback jitter/stretch/tone в звонке. | |
||||
|
||||
`unfilled_frames` — незаполненный output, `hole_frames` — потеря capture timeline, |
||||
`capture_drop_frames` — переполнение capture-ring, `no_progress` — PA-write без |
||||
продвижения. `backend_underruns` — отдельный callback самого PA-stream; это не |
||||
то же самое, что пустой сетевой jitter. `backend playback started/resumed` |
||||
показывает фактическое начало/возобновление playback. Notification содержит |
||||
числовые miniaudio type/state. |
||||
|
||||
Первое событие каждого типа за окно имеет tid и CLOCK_MONOTONIC timestamp; |
||||
повторения суммируются. Для задержавшихся потоков reporter читает `/proc` wchan |
||||
и накопленные schedstat CPU/ready-wait/slices. Сравнивать соседние snapshots: |
||||
сами totals не являются длительностью текущей паузы. CPU-time намного меньше |
||||
wall-time означает ожидание/вытеснение; wchan помогает отличить poll от mutex/ |
||||
I/O wait. Это снимок в момент отчёта, не исторический стек зависшего вызова. |
||||
|
||||
Producer не выделяет память, не ждёт mutex reporter и не пишет диагностические |
||||
логи: ограниченная MPSC-очередь 8192 события, не более четырёх CAS-попыток. |
||||
Reporter не разыменовывает ma_device/pa_stream. Переполнение логируется как |
||||
`diagnostic events lost`; такое окно нельзя считать полным. Инструментация |
||||
добавляет получение часов/TID и атомарные операции, поэтому overhead ненулевой. |
||||
Частые события call jitter/AEC/TX теперь публикуются счётчиками и выводятся из |
||||
uasync вне аудиомьютекса. Часть существующих логов рации остаётся синхронной; |
||||
тракт не объявляется строго realtime-safe. |
||||
После close устройство удаляется из reporter: для устройств короче секунды |
||||
доступны lifecycle и первые аномалии, но не полная секундная сводка. |
||||
|
||||
Новый binary собран в `tools/chatgui/build/vibechat`; для нового измерения нужен |
||||
его обычный перезапуск. Изменение файла binary не обновляет уже запущенный процесс. |
||||
|
||||
## Реализация |
||||
|
||||
`AudioDevice` — небольшой владелец одного miniaudio device в GUI-потоке. |
||||
Успешный init всегда имеет парный uninit, включая ошибку start. Callback и его |
||||
данные готовятся до start. Close останавливает/join-ит worker перед очисткой |
||||
callback. Прогресс — атомарный счётчик завершённых кадров, независимый от reporter. |
||||
Каждый владелец проверяет свои устройства Qt-таймером 200 мс: stopped либо 1 с |
||||
без продвижения требуют пересоздания. Suspend обновляет grace; единичный xrun |
||||
не запускает restart. Backoff: 250/500/1000/2000 мс, сброс после прогресса. |
||||
|
||||
Capture и playback звонка теперь независимы. Период выбирает backend; аппаратный |
||||
чанк не обязан совпадать с 20-мс Opus-кадром. Рация заполняет большой output |
||||
несколькими pulls по максимум 20 мс. AEC сохраняет весь render, включая хвосты |
||||
1200/720 samples, а capture принимает только полный кадр. При аппаратном recovery |
||||
после join сбрасываются capture accumulator, AEC и локальный jitter/stretch; |
||||
из encoded backlog сохраняется только свежий резерв. Call ID и группа сохраняются. |
||||
|
||||
`SoundManager` отдельно владеет общим контекстом и устройством вывода. |
||||
`ma_engine` работает без своего устройства: mixer, звуки и PCM cursor переживают |
||||
recovery. При failed context синхронный сигнал сначала закрывает всех клиентов, |
||||
затем context освобождается. После повторного init клиенты открываются по desired |
||||
state. Stop выключает recovery; таймер завершения звонка проверяет generation. |
||||
MainWindow останавливает звонок/рацию/recorder до SoundManager и сетевого ядра. |
||||
Регистрация desktop voice ops выполняется в потоке ядра. |
||||
|
||||
PulseAudio control-waits ограничены 1 с на операцию, mainloop iterates неблокирующие. |
||||
При timeout pending operation отменяется до возврата из функции, чтобы callback |
||||
не получил просроченный stack pointer. Partial init закрывает PA context/mainloop |
||||
и события miniaudio. Device worker распознаёт failed context/stream; лог содержит |
||||
состояния и PA errno. Capture hole подаётся как тишина той же длительности; |
||||
zero writable/zero converter progress возвращают управление. Duplex output заранее |
||||
полностью инициализируется, starvation не выключает playback callback. |
||||
|
||||
TX ограничен восемью задачами до помещения в uasync, возраст — максимум 200 мс. |
||||
Generation исключает передачу кадров старого звонка/захвата. Ещё не обработанные |
||||
старые задачи учитываются и при повторном start, поэтому очередь ограничена |
||||
между поколениями. Task и аргументы имеют одну аллокацию; destroy очереди освобождает |
||||
их и для media, и для PTT control. Bad Opus в звонке учитывается в бюджете decode |
||||
на pull. Ошибки encode/очереди/выделения памяти/возраста различаются в сводке TX. |
||||
|
||||
Устройства сохраняются как backend ID вместо индекса. При init/start failure |
||||
явного выбора пробуется default, причина логируется. Выбранный ID сохраняется; |
||||
вернуться к нему можно повторным выбором или при следующем recovery, автоматический |
||||
опрос hotplug не добавлен. Recorder использует pUserData своего устройства, |
||||
компрессор настраивается до записи, UI получает атомарный уровень и длительность |
||||
по записанным кадрам. Настройки компрессора в ходе записи применяются к следующей. |
||||
|
||||
## Проверки реализации |
||||
|
||||
`cmake --build tools/chatgui/build -j4` — успешно. |
||||
|
||||
```sh |
||||
ctest --test-dir tools/chatgui/build --output-on-failure \ |
||||
-R 'test_(linux_audio|audio_diagnostics|audio_recovery|call_jitter|radio_audio|radio_jitter|voice_file)$' |
||||
``` |
||||
|
||||
7/7 успешно. Проверены целостность MPSC, bounded overflow/restart reporter, |
||||
duplex starvation, PA zero-progress, capture hole, deadline/cancel, сохранение |
||||
AEC render, bad Opus budget, partial start/init, две записи одновременно, |
||||
восстановление только playback, потеря общего context и повторные отказы init, |
||||
stop во время backoff, старый end timer, сохранение PCM cursor, selected→default, |
||||
TX limit между поколениями и освобождение pending PTT при destroy. |
||||
|
||||
Изолированная Debug-сборка в `/tmp/utun-audio-asan`: четыре теста |
||||
`audio_recovery/linux_audio/call_jitter/radio_audio` проходят с ASAN/UBSAN и |
||||
LeakSanitizer. Запуск вне песочницы нужен LeakSanitizer для проверки потоков. |
||||
|
||||
Реальный перезапуск сервера: |
||||
|
||||
```sh |
||||
python3 tools/chatgui/tests/run_pulse_recovery.py |
||||
``` |
||||
|
||||
Тест создаёт собственные PipeWire, PulseAudio и WirePlumber в `/tmp`, отключает |
||||
аппаратные monitors, использует виртуальный sink/monitor source. После остановки |
||||
PulseAudio проверяет освобождение контекста; после запуска — новый прогресс |
||||
capture и прежнего PCM sound. Проверка прошла: capture 325→575 мс, playback |
||||
16384→29184 frames. Системный звуковой сервер не перезапускается. |
||||
|
||||
Полный `check.sh` до исключения async-тестов: 119 passed, 0 failed, 1 skipped; |
||||
proxy/burst/load интеграции прошли. По последующему указанию пользователя async |
||||
тесты исключены из дальнейших запусков. `./gradlew --offline assembleDebug` — |
||||
успешно, APK не установлен на телефон. |
||||
|
||||
Физический USB/Bluetooth hotplug, сон ОС и качество длительного реального звонка |
||||
этими проверками не подтверждены. AEC reference отдельных уведомлений и компенсация |
||||
дрейфа часов звонка остаются ограничениями исходного DSP. Граница 1 с относится |
||||
к отдельной PA-операции: последовательность init/uninit нескольких устройств |
||||
может задержать GUI дольше. Дополнительный owner-thread/DSP-worker не добавлен. |
||||
|
||||
## Референсы |
||||
|
||||
Первичные исходники и документация просмотрены 2026-10-01; ссылки на master могут |
||||
изменяться. Используем принципы, а не копируем целиком архитектуру библиотеки. |
||||
|
||||
- [PipeWire audio-src-ring.c](https://docs.pipewire.org/audio-src-ring_8c-example.html): |
||||
callback читает доступные кадры, заполняет остаток тишиной и возвращает полный |
||||
буфер; пополнение инициирует через событие управляющего потока. Хороший минимальный |
||||
пример поведения при пустой PCM-очереди. |
||||
- [PulseAudio stream API](https://github.com/pulseaudio/pulseaudio/blob/master/src/pulse/stream.h): |
||||
отдельные underflow/overflow/started/suspended/moved callbacks; peek различает |
||||
пустую очередь и hole с ненулевой длиной. Нужно разделять эти состояния в логах. |
||||
- [PulseAudio pacat.c](https://github.com/pulseaudio/pulseaudio/blob/master/src/utils/pacat.c): |
||||
underflow callback сообщает событие без пересоздания stream. При failed context |
||||
утилита завершает работу — это пример диагностики, не готовая self-healing система. |
||||
- [Mozilla cubeb PulseAudio backend](https://github.com/mozilla/cubeb/blob/master/src/cubeb_pulse.c): |
||||
явные context/stream states, пересоздание ошибочного context при инициализации, |
||||
согласованное отключение callbacks и доступ под mainloop lock; minimum latency |
||||
25 мс. Это полезный референс владения и границ управления, а не основание |
||||
автоматически заимствовать всю библиотеку. |
||||
- [miniaudio](https://github.com/mackron/miniaudio/blob/master/miniaudio.h): |
||||
init/start/stop/uninit запрещены внутри data callback из-за deadlock. Просмотренный |
||||
upstream тоже содержит break при пустом duplex-ring: обновление библиотеки само |
||||
по себе не решает этот найденный дефект. |
||||
@ -0,0 +1,169 @@
|
||||
#define _GNU_SOURCE |
||||
#include "async_regression.h" |
||||
#include <dirent.h> |
||||
#include <errno.h> |
||||
#include <limits.h> |
||||
#include <signal.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <sys/eventfd.h> |
||||
#include <sys/resource.h> |
||||
#include <sys/wait.h> |
||||
#include <time.h> |
||||
#include <unistd.h> |
||||
|
||||
static int alloc_mask, alloc_nth, alloc_hit, alloc_persistent; |
||||
static const char* alloc_scope; |
||||
static int64_t clock_shift_ms; |
||||
static struct epoll_event pending_events[64]; |
||||
static int pending_count, pending_active, fail_ctl, fail_eventfd, fail_epoll_create; |
||||
|
||||
void regression_fault_arm(int mask, const char* scope, int nth) { |
||||
alloc_mask = mask; alloc_scope = scope; alloc_nth = nth; alloc_hit = 0; alloc_persistent = nth == 0; |
||||
} |
||||
|
||||
int regression_fault_hit(void) { return alloc_hit; } |
||||
void regression_fault_reset(void) { alloc_mask = 0; } |
||||
void regression_clock_shift(int64_t milliseconds) { clock_shift_ms = milliseconds; } |
||||
void regression_epoll_ctl_fail(int operation) { fail_ctl = operation; } |
||||
void regression_eventfd_fail(void) { fail_eventfd = 1; } |
||||
void regression_epoll_create_fail(void) { fail_epoll_create = 1; } |
||||
|
||||
static int fail_allocation(int kind, const char* location) { |
||||
if (!(alloc_mask & kind) || (alloc_scope && !strstr(location, alloc_scope))) return 0; |
||||
// nth=0: устойчивый отказ всех подходящих выделений до reset.
|
||||
if (!alloc_persistent && --alloc_nth != 0) return 0; |
||||
alloc_hit = 1; |
||||
fprintf(stderr, "[INJECT] allocation failure: kind=%d at %s\n", kind, location); |
||||
return 1; |
||||
} |
||||
|
||||
void* __real_u_malloc_impl(uint32_t size, const char* location); |
||||
void* __real_u_calloc_impl(uint32_t count, uint32_t size, const char* location); |
||||
void* __real_u_realloc_impl(void* ptr, uint32_t size, const char* location); |
||||
|
||||
void* __wrap_u_malloc_impl(uint32_t size, const char* location) { |
||||
return fail_allocation(FAULT_MALLOC, location) ? NULL : __real_u_malloc_impl(size, location); |
||||
} |
||||
void* __wrap_u_calloc_impl(uint32_t count, uint32_t size, const char* location) { |
||||
return fail_allocation(FAULT_CALLOC, location) ? NULL : __real_u_calloc_impl(count, size, location); |
||||
} |
||||
void* __wrap_u_realloc_impl(void* ptr, uint32_t size, const char* location) { |
||||
return fail_allocation(FAULT_REALLOC, location) ? NULL : __real_u_realloc_impl(ptr, size, location); |
||||
} |
||||
|
||||
int __real_gettimeofday(struct timeval* tv, void* tz); |
||||
int __wrap_gettimeofday(struct timeval* tv, void* tz) { |
||||
int rc = __real_gettimeofday(tv, tz); |
||||
if (rc == 0 && clock_shift_ms) { |
||||
int64_t us = (int64_t)tv->tv_sec * 1000000 + tv->tv_usec + clock_shift_ms * 1000; |
||||
tv->tv_sec = us / 1000000; tv->tv_usec = us % 1000000; |
||||
} |
||||
return rc; |
||||
} |
||||
|
||||
void regression_epoll_events(const struct epoll_event* events, int count) { |
||||
if (count < 0 || count > 64) abort(); |
||||
memcpy(pending_events, events, count * sizeof(*events)); |
||||
pending_count = count; pending_active = 1; |
||||
} |
||||
int __real_epoll_wait(int fd, struct epoll_event* events, int maxevents, int timeout); |
||||
int __wrap_epoll_wait(int fd, struct epoll_event* events, int maxevents, int timeout) { |
||||
if (!pending_active) return __real_epoll_wait(fd, events, maxevents, timeout); |
||||
if (pending_count > maxevents) abort(); |
||||
memcpy(events, pending_events, pending_count * sizeof(*events)); |
||||
pending_active = 0; |
||||
return pending_count; |
||||
} |
||||
int __real_epoll_ctl(int epfd, int op, int fd, struct epoll_event* event); |
||||
int __wrap_epoll_ctl(int epfd, int op, int fd, struct epoll_event* event) { |
||||
if (fail_ctl == op) { fail_ctl = 0; errno = ENOMEM; return -1; } |
||||
return __real_epoll_ctl(epfd, op, fd, event); |
||||
} |
||||
int __real_eventfd(unsigned int initval, int flags); |
||||
int __wrap_eventfd(unsigned int initval, int flags) { |
||||
if (fail_eventfd) { fail_eventfd = 0; errno = EMFILE; return -1; } |
||||
return __real_eventfd(initval, flags); |
||||
} |
||||
int __real_epoll_create1(int flags); |
||||
int __wrap_epoll_create1(int flags) { |
||||
if (fail_epoll_create) { fail_epoll_create = 0; errno = EMFILE; return -1; } |
||||
return __real_epoll_create1(flags); |
||||
} |
||||
|
||||
uint64_t regression_time_ms(void) { |
||||
struct timespec ts; |
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) abort(); |
||||
return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; |
||||
} |
||||
|
||||
void regression_log_init(void) { |
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); |
||||
for (int i = 1; i < DEBUG_CATEGORY_COUNT; i++) debug_set_category_level(i, DEBUG_LEVEL_WARN); |
||||
if (getenv("UTUN_TEST_DEBUG")) { |
||||
debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_DEBUG); |
||||
debug_set_category_level(DEBUG_CATEGORY_SOCKET, DEBUG_LEVEL_DEBUG); |
||||
debug_set_category_level(DEBUG_CATEGORY_TIMERS, DEBUG_LEVEL_DEBUG); |
||||
} |
||||
} |
||||
|
||||
static int open_fds(void) { |
||||
DIR* dir = opendir("/proc/self/fd"); |
||||
if (!dir) return -1; |
||||
int count = 0; |
||||
struct dirent* entry; |
||||
while ((entry = readdir(dir))) if (entry->d_name[0] != '.') count++; |
||||
closedir(dir); |
||||
return count; |
||||
} |
||||
|
||||
int regression_run(int argc, char** argv, const struct regression_case* cases, size_t count) { |
||||
if (argc > 2) { fprintf(stderr, "Usage: %s [case-name-substring]\n", argv[0]); return 1; } |
||||
setvbuf(stdout, NULL, _IONBF, 0); |
||||
int passed = 0, failed = 0, skipped = 0; |
||||
for (size_t i = 0; i < count; i++) { |
||||
if (argc == 2 && !strstr(cases[i].name, argv[1])) continue; |
||||
printf("[RUN] %s\n", cases[i].name); |
||||
int pipefd[2]; |
||||
if (pipe(pipefd) < 0) { perror("regression pipe"); return 1; } |
||||
pid_t pid = fork(); |
||||
if (pid < 0) { perror("regression fork"); close(pipefd[0]); close(pipefd[1]); return 1; } |
||||
if (pid == 0) { |
||||
close(pipefd[0]); |
||||
struct rlimit core = {0, 0}; setrlimit(RLIMIT_CORE, &core); |
||||
regression_log_init(); |
||||
size_t allocations = u_get_allocated_count(); |
||||
int fds = open_fds(); |
||||
int rc = cases[i].run(cases[i].param); |
||||
if (rc == 0 && (allocations != u_get_allocated_count() || fds != open_fds())) { |
||||
fprintf(stderr, "[FAIL] resource leak: allocations=%zu->%zu fds=%d->%d\n", |
||||
allocations, u_get_allocated_count(), fds, open_fds()); |
||||
u_report_unfreed_blocks(); rc = 1; |
||||
} |
||||
close(pipefd[1]); |
||||
_exit(rc); |
||||
} |
||||
close(pipefd[1]); |
||||
struct pollfd p = { .fd = pipefd[0], .events = POLLIN }; |
||||
uint64_t deadline = regression_time_ms() + 2000; |
||||
int ready; |
||||
do { |
||||
uint64_t now = regression_time_ms(); |
||||
int remaining = deadline > now ? (int)(deadline - now) : 0; |
||||
ready = poll(&p, 1, remaining); |
||||
} while (ready < 0 && errno == EINTR && regression_time_ms() < deadline); |
||||
int timed_out = ready <= 0; |
||||
if (timed_out && kill(pid, SIGKILL) < 0 && errno != ESRCH) perror("regression kill"); |
||||
close(pipefd[0]); |
||||
int status; |
||||
while (waitpid(pid, &status, 0) < 0) if (errno != EINTR) { perror("regression waitpid"); return 1; } |
||||
if (timed_out) { printf("[FAIL] %s: timeout (2000ms)\n", cases[i].name); failed++; } |
||||
else if (WIFSIGNALED(status)) { printf("[FAIL] %s: signal %d\n", cases[i].name, WTERMSIG(status)); failed++; } |
||||
else if (WEXITSTATUS(status) == 77) { printf("[SKIP] %s\n", cases[i].name); skipped++; } |
||||
else if (WEXITSTATUS(status)) { printf("[FAIL] %s: exit %d\n", cases[i].name, WEXITSTATUS(status)); failed++; } |
||||
else { printf("[PASS] %s\n", cases[i].name); passed++; } |
||||
} |
||||
printf("Test Results: %d passed, %d failed, %d skipped\n", passed, failed, skipped); |
||||
if (!passed && !failed && !skipped) { fprintf(stderr, "No matching cases\n"); return 1; } |
||||
return failed ? 1 : 0; |
||||
} |
||||
@ -0,0 +1,41 @@
|
||||
/* Изоляция регрессий: падение/зависание одного случая не скрывает остальные. */ |
||||
#ifndef ASYNC_REGRESSION_H |
||||
#define ASYNC_REGRESSION_H |
||||
|
||||
#include "../lib/u_async.h" |
||||
#include "../lib/ll_queue.h" |
||||
#include "../lib/mem.h" |
||||
#include <stdio.h> |
||||
#include <sys/epoll.h> |
||||
|
||||
#define CHECK(cond) do { if (!(cond)) { \ |
||||
fprintf(stderr, "[FAIL] %s:%d: %s\n", __FILE__, __LINE__, #cond); return 1; \
|
||||
} } while (0) |
||||
#define CHECK_EQ(actual, expected) do { \ |
||||
long long a_ = (long long)(actual), e_ = (long long)(expected); \
|
||||
if (a_ != e_) { fprintf(stderr, "[FAIL] %s:%d: %s=%lld expected=%lld\n", \
|
||||
__FILE__, __LINE__, #actual, a_, e_); return 1; } \
|
||||
} while (0) |
||||
|
||||
struct regression_case { |
||||
const char* name; |
||||
int (*run)(int param); |
||||
int param; |
||||
}; |
||||
|
||||
int regression_run(int argc, char** argv, const struct regression_case* cases, size_t count); |
||||
uint64_t regression_time_ms(void); |
||||
void regression_log_init(void); |
||||
|
||||
enum regression_alloc { FAULT_MALLOC = 1, FAULT_CALLOC = 2, FAULT_REALLOC = 4, FAULT_ALLOC = 7 }; |
||||
// nth=0 — устойчивый отказ, nth>0 — отказ только соответствующего выделения.
|
||||
void regression_fault_arm(int mask, const char* scope, int nth); |
||||
int regression_fault_hit(void); |
||||
void regression_fault_reset(void); |
||||
void regression_clock_shift(int64_t milliseconds); |
||||
void regression_epoll_events(const struct epoll_event* events, int count); |
||||
void regression_epoll_ctl_fail(int operation); |
||||
void regression_eventfd_fail(void); |
||||
void regression_epoll_create_fail(void); |
||||
|
||||
#endif |
||||
@ -0,0 +1,134 @@
|
||||
/* Реальный Android UDP-логгер: несколько producers, смена адреса, restart и
|
||||
* reentrant debug hook. Все операции с таймерами обязаны идти в owner thread. */ |
||||
#include "../tools/chatgui-android/jni_bridge/android_udp_log.h" |
||||
#include "../lib/u_async.h" |
||||
#include "../lib/debug_config.h" |
||||
#include "../lib/mem.h" |
||||
#include <assert.h> |
||||
#include <limits.h> |
||||
#include <pthread.h> |
||||
#include <stdio.h> |
||||
#include <string.h> |
||||
#include <unistd.h> |
||||
|
||||
static pthread_t owner; |
||||
static int fail_timer; |
||||
static unsigned timer_sets, timer_cancels; |
||||
|
||||
void* __real_uasync_set_timeout(struct UASYNC*, int, void*, timeout_callback_t, const char*); |
||||
err_t __real_uasync_cancel_timeout(struct UASYNC*, void*); |
||||
|
||||
void* __wrap_uasync_set_timeout(struct UASYNC* ua, int tb, void* arg, timeout_callback_t cb, const char* name) { |
||||
assert(pthread_equal(owner, pthread_self())); |
||||
timer_sets++; |
||||
if (fail_timer) { |
||||
fail_timer = 0; |
||||
DEBUG_ERROR(DEBUG_CATEGORY_TIMERS, "injected flush timer allocation failure"); |
||||
return NULL; |
||||
} |
||||
return __real_uasync_set_timeout(ua, tb, arg, cb, name); |
||||
} |
||||
|
||||
err_t __wrap_uasync_cancel_timeout(struct UASYNC* ua, void* timer) { |
||||
assert(pthread_equal(owner, pthread_self())); |
||||
timer_cancels++; |
||||
return __real_uasync_cancel_timeout(ua, timer); |
||||
} |
||||
|
||||
struct producer { unsigned id; int port; }; |
||||
|
||||
static void* produce(void* arg) { |
||||
struct producer* p = arg; |
||||
char line[32]; memset(line, 'a' + p->id, sizeof(line)); line[31] = '\n'; |
||||
for (unsigned i = 0; i < 1000; i++) { |
||||
udp_log_write(line, sizeof(line)); |
||||
if (!p->id && i % 250 == 0) udp_log_set_target("127.0.0.1", p->port); |
||||
if (i % 32 == 0) usleep(500); |
||||
} |
||||
return NULL; |
||||
} |
||||
|
||||
static unsigned receive_lines(int fd) { |
||||
char buf[65536]; |
||||
unsigned count = 0; |
||||
ssize_t len; |
||||
while ((len = recv(fd, buf, sizeof(buf), MSG_DONTWAIT)) > 0) { |
||||
assert(len % 32 == 0); |
||||
for (ssize_t off = 0; off < len; off += 32) { |
||||
assert(buf[off] >= 'a' && buf[off] <= 'd' && buf[off + 31] == '\n'); |
||||
for (int i = 1; i < 31; i++) assert(buf[off + i] == buf[off]); |
||||
count++; |
||||
} |
||||
} |
||||
return count; |
||||
} |
||||
|
||||
static void forward_log(int level, const char* category, const char* message) { |
||||
(void)category; |
||||
udp_log_write(message, (int)strlen(message)); |
||||
if (level == DEBUG_LEVEL_ERROR) udp_log_flush(); |
||||
} |
||||
|
||||
int main(void) { |
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); |
||||
owner = pthread_self(); |
||||
int fd = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0); assert(fd >= 0); |
||||
int buffer_size = 1024 * 1024; |
||||
assert(setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &buffer_size, sizeof(buffer_size)) == 0); |
||||
struct sockaddr_in addr = {0}; addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
||||
assert(bind(fd, (struct sockaddr*)&addr, sizeof(addr)) == 0); |
||||
socklen_t addr_len = sizeof(addr); |
||||
assert(getsockname(fd, (struct sockaddr*)&addr, &addr_len) == 0); |
||||
int port = ntohs(addr.sin_port); |
||||
struct UASYNC* ua = uasync_create(); assert(ua); |
||||
udp_log_set_target("127.0.0.1", port); udp_log_attach(ua); |
||||
pthread_t threads[4]; struct producer producers[4] = {0}; |
||||
for (unsigned i = 0; i < 4; i++) { |
||||
producers[i].id = i; producers[i].port = port; |
||||
assert(pthread_create(&threads[i], NULL, produce, &producers[i]) == 0); |
||||
} |
||||
unsigned received = 0; |
||||
uint64_t deadline = get_time_tb() + 30000; |
||||
while (received < 4000 && get_time_tb() < deadline) { |
||||
uasync_poll(ua, 10); |
||||
received += receive_lines(fd); |
||||
} |
||||
for (int i = 0; i < 4; i++) assert(pthread_join(threads[i], NULL) == 0); |
||||
assert(received == 4000); |
||||
assert(timer_sets && ua->timer_alloc_count - ua->timer_free_count == 1); |
||||
puts("PASS: 4000 records from 4 threads, target changes and timed flush without torn records"); |
||||
|
||||
/* Команда в очереди перед detach не должна обращаться к уничтоженному ua. */ |
||||
udp_log_set_target("127.0.0.1", port); |
||||
udp_log_detach(ua); uasync_poll(ua, 0); |
||||
assert(ua->timer_alloc_count == ua->timer_free_count); |
||||
uasync_destroy(ua, 0); |
||||
unsigned detached_sets = timer_sets; |
||||
udp_log_write("detached\n", 9); |
||||
udp_log_set_target("127.0.0.1", port); |
||||
assert(timer_sets == detached_sets); |
||||
udp_log_stop(); |
||||
char error_log[4096]; |
||||
while (recv(fd, error_log, sizeof(error_log), MSG_DONTWAIT) > 0) {} |
||||
|
||||
/* Ошибка установки таймера логируется через тот же hook, без deadlock. */ |
||||
ua = uasync_create(); assert(ua); |
||||
debug_set_log_hook(forward_log); |
||||
udp_log_set_target("127.0.0.1", port); fail_timer = 1; udp_log_attach(ua); |
||||
ssize_t error_len = recv(fd, error_log, sizeof(error_log) - 1, MSG_DONTWAIT); |
||||
assert(error_len > 0); error_log[error_len] = 0; |
||||
assert(strstr(error_log, "injected flush timer allocation failure")); |
||||
udp_log_set_target("127.0.0.1", port); uasync_poll(ua, 0); |
||||
assert(ua->timer_alloc_count - ua->timer_free_count == 1); |
||||
udp_log_write("x", INT_MAX); /* Отказ до чтения данных, без переполнения длины. */ |
||||
udp_log_set_target(NULL, 0); uasync_poll(ua, 0); |
||||
assert(ua->timer_alloc_count == ua->timer_free_count); |
||||
udp_log_set_target("127.0.0.1", port); uasync_poll(ua, 0); |
||||
assert(ua->timer_alloc_count - ua->timer_free_count == 1); |
||||
udp_log_detach(ua); udp_log_stop(); uasync_poll(ua, 0); |
||||
assert(ua->timer_alloc_count == ua->timer_free_count && timer_cancels); |
||||
debug_set_log_hook(NULL); |
||||
uasync_destroy(ua, 0); close(fd); |
||||
puts("PASS: detach/restart, disable/re-enable, oversized log and reentrant error hook"); |
||||
return 0; |
||||
} |
||||
@ -0,0 +1,402 @@
|
||||
/* Проверяем очередь независимой моделью и контрактами callbacks/владения. */ |
||||
#include "async_regression.h" |
||||
#include <stddef.h> |
||||
#include <string.h> |
||||
|
||||
static void count_cb(struct ll_queue* q, void* arg) { (void)q; ++*(int*)arg; } |
||||
static struct ll_entry* new_entry(int value, int len) { |
||||
struct ll_entry* e = queue_entry_new(sizeof(value)); |
||||
if (e) { memcpy(e->data, &value, sizeof(value)); e->len = len; } |
||||
return e; |
||||
} |
||||
static int entry_value(struct ll_entry* e) { int value; memcpy(&value, e->data, sizeof(value)); return value; } |
||||
static void dispose(struct ll_queue* q) { |
||||
queue_set_callback(q, NULL, NULL); queue_set_on_get(q, NULL, NULL); queue_set_empty_callback(q, NULL, NULL); |
||||
struct ll_entry* e; |
||||
while ((e = queue_data_get(q))) { queue_dgram_free(e); queue_entry_free(e); } |
||||
queue_free(q); |
||||
} |
||||
|
||||
static int test_empty_notification(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "empty notification"); CHECK(q); |
||||
int calls = 0; |
||||
if (param == 0 || param == 1) queue_set_empty_callback(q, count_cb, &calls); |
||||
struct ll_entry* e = new_entry(1, 12); CHECK(e); CHECK_EQ(queue_data_put(q, e), 0); |
||||
if (param == 0) uasync_drain_immediate(ua); /* Очередь успела заполниться до вызова. */ |
||||
if (param >= 2) queue_set_empty_callback(q, count_cb, &calls); |
||||
if (param == 3) { CHECK_EQ(queue_remove_data(q, e), 0); queue_entry_free(e); } |
||||
else queue_entry_free(queue_data_get(q)); |
||||
CHECK_EQ(calls, 0); uasync_drain_immediate(ua); CHECK_EQ(calls, 1); |
||||
uasync_drain_immediate(ua); CHECK_EQ(calls, 1); |
||||
dispose(q); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_empty_cancel_replace(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "empty cancellation"); CHECK(q); |
||||
int old = 0, current = 0; |
||||
queue_set_empty_callback(q, count_cb, &old); |
||||
queue_set_empty_callback(q, param ? count_cb : NULL, ¤t); |
||||
uasync_drain_immediate(ua); CHECK_EQ(old, 0); CHECK_EQ(current, param ? 1 : 0); |
||||
dispose(q); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_empty_free(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "empty before free"); CHECK(q); int calls = 0; |
||||
if (param) CHECK_EQ(queue_data_put(q, new_entry(1, 0)), 0); |
||||
queue_set_empty_callback(q, count_cb, &calls); |
||||
if (param) queue_entry_free(queue_data_get(q)); |
||||
queue_free(q); uasync_drain_immediate(ua); CHECK_EQ(calls, 0); |
||||
uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_on_get(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "on get"); CHECK(q); int calls = 0; |
||||
queue_set_on_get(q, count_cb, &calls); |
||||
for (int i = 0; i < 4; i++) CHECK_EQ(queue_data_put(q, new_entry(i, i + 1)), 0); |
||||
for (int i = 0; i < 4; i++) queue_entry_free(queue_data_get(q)); |
||||
CHECK_EQ(calls, 0); |
||||
if (param) queue_set_on_get(q, NULL, NULL); |
||||
uasync_drain_immediate(ua); CHECK_EQ(calls, param ? 0 : 4); |
||||
dispose(q); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_on_get_free(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "on get teardown"); CHECK(q); int calls = 0; |
||||
queue_set_on_get(q, count_cb, &calls); |
||||
for (int i = 0; i < param + 1; i++) CHECK_EQ(queue_data_put(q, new_entry(i, 1)), 0); |
||||
for (int i = 0; i < param + 1; i++) queue_entry_free(queue_data_get(q)); |
||||
queue_set_on_get(q, NULL, NULL); queue_free(q); |
||||
uasync_drain_immediate(ua); CHECK_EQ(calls, 0); /* ASAN проверяет даже чтение cb=NULL из freed q. */ |
||||
uasync_destroy(ua, 0); return 0; |
||||
} |
||||
|
||||
typedef int (*put_fn)(struct ll_queue*, struct ll_entry*); |
||||
static put_fn putters[] = {queue_data_put, queue_data_put_first, queue_data_put_with_index, queue_data_put_first_with_index}; |
||||
static int test_limits(int param) { |
||||
int which = param & 3, limit = param & 4 ? 0 : 1; |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, which >= 2 ? 1 : 0, 0, which >= 2 ? sizeof(int) : 0, "limit"); CHECK(q); |
||||
queue_set_size_limit(q, limit); |
||||
if (limit) CHECK_EQ(putters[which](q, new_entry(1, 30)), 0); |
||||
CHECK_EQ(putters[which](q, new_entry(2, 40)), -1); |
||||
CHECK_EQ(q->count, limit); CHECK_EQ(q->total_bytes, limit ? 30 : 0); |
||||
dispose(q); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_remove_invalid(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 1, 0, sizeof(int), "invalid remove"); CHECK(q); |
||||
struct ll_entry* a = new_entry(1, 100), *b = new_entry(2, 200); CHECK(a && b); |
||||
CHECK_EQ(queue_data_put_with_index(q, a), 0); |
||||
if (param == 1) { |
||||
CHECK_EQ(queue_data_put_with_index(q, b), 0); CHECK_EQ(queue_remove_data(q, b), 0); |
||||
} |
||||
struct ll_queue* other = NULL; |
||||
if (param == 2) { |
||||
other = queue_new(ua, 1, 0, sizeof(int), "other queue"); CHECK(other); |
||||
CHECK_EQ(queue_data_put_with_index(other, b), 0); |
||||
} |
||||
CHECK_EQ(queue_remove_data(q, b), -1); |
||||
CHECK_EQ(q->count, 1); CHECK_EQ(q->total_bytes, 100); CHECK(q->head == a && q->tail == a); |
||||
CHECK_EQ(queue_check_consistency(q), 0); |
||||
if (other) dispose(other); else queue_entry_free(b); |
||||
dispose(q); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
|
||||
struct consume_test { int calls, depth, max_depth, error, action; }; |
||||
static void consume_cb(struct ll_queue* q, void* arg) { |
||||
struct consume_test* t = arg; |
||||
if (++t->depth > t->max_depth) t->max_depth = t->depth; |
||||
struct ll_entry* e = queue_data_get(q); |
||||
if (!e) t->error = 1; else { t->calls++; queue_entry_free(e); } |
||||
if (t->action != 1) queue_resume_callback(q); |
||||
t->depth--; |
||||
} |
||||
static int test_consume(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "consumer"); CHECK(q); |
||||
struct consume_test t = {0}; t.action = param & 2 ? 1 : 0; |
||||
queue_set_callback(q, consume_cb, &t); queue_set_callback_defer(q, param & 1); |
||||
for (int i = 0; i < 8; i++) CHECK_EQ(queue_data_put(q, new_entry(i, 1)), 0); |
||||
if (param & 1) CHECK_EQ(t.calls, 0); |
||||
uasync_drain_immediate(ua); |
||||
if (t.action) { |
||||
CHECK_EQ(t.calls, 1); CHECK_EQ(q->count, 7); |
||||
t.action = 0; queue_resume_callback(q); uasync_drain_immediate(ua); |
||||
} |
||||
CHECK_EQ(t.calls, 8); CHECK_EQ(t.error, 0); CHECK_EQ(t.max_depth, 1); CHECK_EQ(q->count, 0); |
||||
dispose(q); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_consume_free_pending(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "consumer teardown"); CHECK(q); |
||||
struct consume_test t = {0}; queue_set_callback(q, consume_cb, &t); queue_set_callback_defer(q, 1); |
||||
CHECK_EQ(queue_data_put(q, new_entry(1, 1)), 0); |
||||
if (param) queue_set_callback(q, NULL, NULL); |
||||
dispose(q); uasync_drain_immediate(ua); CHECK_EQ(t.calls, 0); |
||||
uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_waiter_cancel(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "waiter cancellation"); CHECK(q); |
||||
queue_set_waiter_defer(q, 1); int calls = 0; struct queue_waiter_handle h = {0}; |
||||
if (param) CHECK_EQ(queue_data_put(q, new_entry(1, 1)), 0); |
||||
CHECK_EQ(queue_waiter_wait(q, &h, count_cb, &calls), param ? 0 : 1); |
||||
if (param == 2) queue_entry_free(queue_data_get(q)); |
||||
queue_waiter_cancel(q, &h); CHECK(!h.internal && !h.call_soon_id); |
||||
uasync_drain_immediate(ua); CHECK_EQ(calls, 0); |
||||
dispose(q); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_waiter_bytes(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "waiter bytes"); CHECK(q); |
||||
int calls = 0; struct queue_waiter_handle h = {0}; queue_set_waiter_defer(q, param); |
||||
queue_set_threshold(q, 10, 20); |
||||
CHECK_EQ(queue_data_put(q, new_entry(1, 20)), 0); CHECK_EQ(queue_data_put(q, new_entry(2, 30)), 0); |
||||
CHECK_EQ(queue_waiter_wait(q, &h, count_cb, &calls), 0); |
||||
queue_entry_free(queue_data_get(q)); uasync_drain_immediate(ua); CHECK_EQ(calls, 0); |
||||
queue_entry_free(queue_data_get(q)); uasync_drain_immediate(ua); CHECK_EQ(calls, 1); |
||||
CHECK(!h.internal && !h.call_soon_id); |
||||
queue_waiter_cancel(q, &h); dispose(q); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_waiter_teardown(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "waiter teardown"); CHECK(q); |
||||
struct queue_waiter_handle h = {0}; int calls = 0; queue_set_waiter_defer(q, 1); |
||||
if (param) CHECK_EQ(queue_data_put(q, new_entry(1, 1)), 0); |
||||
queue_waiter_wait(q, &h, count_cb, &calls); |
||||
if (param == 2) queue_entry_free(queue_data_get(q)); |
||||
queue_waiter_cancel(q, &h); dispose(q); uasync_drain_immediate(ua); CHECK_EQ(calls, 0); |
||||
CHECK(!h.internal && !h.call_soon_id); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
|
||||
/* Teardown без ручной отмены и повторное использование handle до dispatch. */ |
||||
static int test_waiter_auto_teardown(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "automatic teardown"); CHECK(q); |
||||
queue_set_waiter_defer(q, 1); struct queue_waiter_handle h[8] = {0}; int calls = 0; |
||||
if (param) CHECK_EQ(queue_data_put(q, new_entry(1, 1)), 0); |
||||
for (int i = 0; i < 8; i++) CHECK_EQ(queue_waiter_wait(q, &h[i], count_cb, &calls), param ? 0 : 1); |
||||
if (param == 2) queue_entry_free(queue_data_get(q)); |
||||
dispose(q); |
||||
for (int i = 0; i < 8; i++) CHECK(!h[i].internal && !h[i].call_soon_id && !h[i].defer_q); |
||||
uasync_drain_immediate(ua); CHECK_EQ(calls, 0); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_waiter_rearm(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "rearm"); CHECK(q); |
||||
struct queue_waiter_handle h = {0}; int old = 0, current = 0; |
||||
queue_set_waiter_defer(q, 1); CHECK_EQ(queue_waiter_wait(q, &h, count_cb, &old), 1); |
||||
if (param) CHECK_EQ(queue_data_put(q, new_entry(1, 1)), 0); |
||||
CHECK_EQ(queue_waiter_wait(q, &h, count_cb, ¤t), param ? 0 : 1); |
||||
if (param) queue_entry_free(queue_data_get(q)); |
||||
uasync_drain_immediate(ua); CHECK_EQ(old, 0); CHECK_EQ(current, 1); |
||||
CHECK(!h.internal && !h.call_soon_id && !h.defer_q); |
||||
dispose(q); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static void free_queue_cb(struct ll_queue* q, void* arg) { ++*(int*)arg; dispose(q); } |
||||
static int test_callback_free_queue(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
int which = param < 8 ? param & 3 : 0, deferred = param & 4; |
||||
struct ll_queue* q = queue_new(ua, which >= 2 ? 1 : 0, 0, which >= 2 ? sizeof(int) : 0, "callback teardown"); CHECK(q); |
||||
int calls = 0; |
||||
if (param < 8) { |
||||
queue_set_callback(q, free_queue_cb, &calls); queue_set_callback_defer(q, deferred); |
||||
CHECK_EQ(putters[which](q, new_entry(1, 10)), 0); |
||||
} else if (param == 8) queue_set_empty_callback(q, free_queue_cb, &calls); |
||||
else if (param == 9) { |
||||
for (int i = 0; i < 4; i++) CHECK_EQ(queue_data_put(q, new_entry(i, 1)), 0); |
||||
queue_set_on_get(q, free_queue_cb, &calls); |
||||
for (int i = 0; i < 4; i++) queue_entry_free(queue_data_get(q)); |
||||
} else { |
||||
struct queue_waiter_handle h = {0}; queue_set_waiter_defer(q, param == 11); |
||||
CHECK_EQ(queue_data_put(q, new_entry(1, 1)), 0); |
||||
CHECK_EQ(queue_waiter_wait(q, &h, free_queue_cb, &calls), 0); |
||||
queue_entry_free(queue_data_get(q)); uasync_drain_immediate(ua); |
||||
CHECK(!h.internal && !h.call_soon_id && !h.defer_q); |
||||
} |
||||
uasync_drain_immediate(ua); CHECK_EQ(calls, 1); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
|
||||
/* Связный список/хеш сравниваются с простым массивом, без использования internals поиска. */ |
||||
static int test_model(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, param == 0 ? 0 : param == 1 ? 1 : 32, 0, |
||||
param ? sizeof(int) : 0, "model"); CHECK(q); |
||||
struct ll_entry* model[64]; int count = 0; uint32_t random = 0x73519321u; |
||||
for (int step = 0; step < 10000; step++) { |
||||
random = random * 1664525u + 1013904223u; |
||||
int op = random >> 28; |
||||
if (count == 0 || (count < 64 && op < 8)) { |
||||
int value = (random >> 8) % 16, len = random % 1000; |
||||
struct ll_entry* e = new_entry(value, len); CHECK(e); |
||||
int first = op & 1; |
||||
CHECK_EQ(putters[(param ? 2 : 0) + first](q, e), 0); |
||||
if (first) memmove(model + 1, model, count * sizeof(*model)); |
||||
model[first ? 0 : count] = e; count++; |
||||
} else { |
||||
int pos = op & 1 ? random % count : 0; |
||||
struct ll_entry* e = model[pos]; |
||||
if (op & 1) CHECK_EQ(queue_remove_data(q, e), 0); else CHECK(queue_data_get(q) == e); |
||||
memmove(model + pos, model + pos + 1, (count - pos - 1) * sizeof(*model)); count--; |
||||
CHECK(!e->next && !e->prev && !e->hash_next); queue_entry_free(e); |
||||
} |
||||
size_t bytes = 0; |
||||
struct ll_entry* actual = q->head; |
||||
for (int i = 0; i < count; i++) { |
||||
CHECK(actual == model[i]); CHECK(actual->prev == (i ? model[i - 1] : NULL)); |
||||
bytes += model[i]->len; actual = actual->next; |
||||
} |
||||
CHECK(!actual); CHECK_EQ(q->count, count); CHECK_EQ(q->total_bytes, bytes); |
||||
CHECK(q->tail == (count ? model[count - 1] : NULL)); CHECK_EQ(queue_check_consistency(q), 0); |
||||
if (param && step % 31 == 0) for (int key = 0; key < 16; key++) { |
||||
int expected = 0, found = 0; |
||||
for (int i = 0; i < count; i++) if (entry_value(model[i]) == key) expected++; |
||||
struct ll_entry* e = queue_find_data_by_index(q, &key); |
||||
while (e) { |
||||
CHECK_EQ(entry_value(e), key); int present = 0; |
||||
for (int i = 0; i < count; i++) if (model[i] == e) present++; |
||||
CHECK_EQ(present, 1); CHECK(++found <= expected); |
||||
e = queue_find_next_by_index(q, &key, e); |
||||
} |
||||
CHECK_EQ(found, expected); |
||||
} |
||||
} |
||||
dispose(q); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_snapshot_bytes(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "length snapshot"); CHECK(q); |
||||
struct ll_entry* e = new_entry(1, 100); CHECK(e); CHECK_EQ(queue_data_put(q, e), 0); |
||||
e->len = 1; CHECK_EQ(q->total_bytes, 100); |
||||
if (param) CHECK_EQ(queue_remove_data(q, e), 0); else CHECK(queue_data_get(q) == e); |
||||
CHECK_EQ(q->total_bytes, 0); queue_entry_free(e); |
||||
dispose(q); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_index_bounds(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 1, param & 2 ? 3 : 0, param & 2 ? 4 : 0, "invalid index"); CHECK(q); |
||||
CHECK_EQ(putters[2 + (param & 1)](q, new_entry(1, 30)), -1); |
||||
CHECK_EQ(q->count, 0); CHECK_EQ(q->total_bytes, 0); |
||||
dispose(q); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_binary_index(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 1, 3, 7, "binary index"); CHECK(q); |
||||
const unsigned char key[] = {0, 1, 0xff, 0x80, 2, 0, 3}; |
||||
struct ll_entry* e = queue_entry_new(10); CHECK(e); memcpy(e->data + 3, key, sizeof(key)); |
||||
CHECK_EQ(putters[2 + param](q, e), 0); CHECK(queue_find_data_by_index(q, key) == e); |
||||
CHECK_EQ(queue_remove_data(q, e), 0); CHECK(!queue_find_data_by_index(q, key)); queue_entry_free(e); |
||||
dispose(q); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int custom_frees; |
||||
static void custom_free(uint8_t* ptr) { custom_frees++; u_free(ptr); } |
||||
static int test_dgram_ownership(int param) { |
||||
struct ll_entry* e = queue_entry_new(0); CHECK(e); e->len = 10; |
||||
struct memory_pool* pool = NULL; |
||||
if (param == 1) { |
||||
pool = memory_pool_init(32, "dgram ownership"); CHECK(pool); |
||||
e->dgram_pool = pool; e->dgram = memory_pool_alloc(pool); |
||||
} else { |
||||
e->dgram = u_malloc(32); if (param == 2) e->dgram_free_fn = custom_free; |
||||
} |
||||
CHECK(e->dgram); queue_dgram_free(e); queue_dgram_free(e); |
||||
CHECK(!e->dgram); CHECK_EQ(e->len, 0); CHECK_EQ(custom_frees, param == 2 ? 1 : 0); |
||||
queue_entry_free(e); if (pool) memory_pool_destroy(pool); return 0; |
||||
} |
||||
|
||||
static int test_creation_oom(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
regression_fault_arm(FAULT_CALLOC, "ll_queue.c:", param); |
||||
CHECK(!queue_new(ua, 8, 0, sizeof(int), "creation oom")); |
||||
CHECK(regression_fault_hit()); regression_fault_reset(); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_entry_oom(int param) { |
||||
regression_fault_arm(FAULT_MALLOC, "ll_queue.c:", param ? 2 : 1); |
||||
CHECK(!(param ? ll_alloc_lldgram(32) : queue_entry_new(8))); |
||||
CHECK(regression_fault_hit()); regression_fault_reset(); return 0; |
||||
} |
||||
static int test_waiter_oom(int param) { |
||||
(void)param; |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "waiter oom"); CHECK(q); |
||||
CHECK_EQ(queue_data_put(q, new_entry(1, 1)), 0); int calls = 0; struct queue_waiter_handle h = {0}; |
||||
regression_fault_arm(FAULT_MALLOC, "ll_queue.c:", 1); |
||||
CHECK_EQ(queue_waiter_wait(q, &h, count_cb, &calls), -1); |
||||
CHECK(regression_fault_hit()); regression_fault_reset(); CHECK(!h.internal); |
||||
CHECK_EQ(queue_waiter_wait(q, &h, count_cb, &calls), 0); |
||||
queue_entry_free(queue_data_get(q)); CHECK_EQ(calls, 1); |
||||
queue_waiter_cancel(q, &h); dispose(q); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
|
||||
static int test_waiter_schedule_oom(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "waiter dispatch oom"); CHECK(q); |
||||
queue_set_waiter_defer(q, 1); struct queue_waiter_handle h = {0}; int calls = 0; |
||||
if (param) { |
||||
queue_set_threshold(q, 1, 0); |
||||
CHECK_EQ(queue_data_put(q, new_entry(1, 1)), 0); CHECK_EQ(queue_data_put(q, new_entry(2, 1)), 0); |
||||
CHECK_EQ(queue_waiter_wait(q, &h, count_cb, &calls), 0); |
||||
} |
||||
regression_fault_arm(FAULT_CALLOC, "u_async.c:", 1); |
||||
if (param) queue_entry_free(queue_data_get(q)); |
||||
else CHECK_EQ(queue_waiter_wait(q, &h, count_cb, &calls), -1); |
||||
CHECK(regression_fault_hit()); regression_fault_reset(); |
||||
if (param) { CHECK(h.internal); queue_entry_free(queue_data_get(q)); } |
||||
else { CHECK(!h.internal && !h.call_soon_id); CHECK_EQ(queue_waiter_wait(q, &h, count_cb, &calls), 1); } |
||||
uasync_drain_immediate(ua); CHECK_EQ(calls, 1); |
||||
CHECK(!h.internal && !h.call_soon_id && !h.defer_q); |
||||
dispose(q); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
|
||||
#define CASE(name, fn, p) {name, fn, p} |
||||
int main(int argc, char** argv) { |
||||
const struct regression_case cases[] = { |
||||
CASE("empty/refill-before-dispatch", test_empty_notification, 0), |
||||
CASE("empty/drain-before-dispatch", test_empty_notification, 1), |
||||
CASE("empty/drain-get", test_empty_notification, 2), CASE("empty/drain-remove", test_empty_notification, 3), |
||||
CASE("empty/cancel", test_empty_cancel_replace, 0), CASE("empty/replace", test_empty_cancel_replace, 1), |
||||
CASE("empty/free-pending-empty", test_empty_free, 0), CASE("empty/free-pending-get", test_empty_free, 1), |
||||
CASE("on-get/each-entry", test_on_get, 0), CASE("on-get/cancel", test_on_get, 1), |
||||
CASE("on-get/free-one-pending", test_on_get_free, 0), CASE("on-get/free-many-pending", test_on_get_free, 3), |
||||
CASE("limit/put", test_limits, 0), CASE("limit/put-first", test_limits, 1), |
||||
CASE("limit/put-index", test_limits, 2), CASE("limit/put-first-index", test_limits, 3), |
||||
CASE("limit/zero-put", test_limits, 4), CASE("limit/zero-put-first", test_limits, 5), |
||||
CASE("limit/zero-put-index", test_limits, 6), CASE("limit/zero-put-first-index", test_limits, 7), |
||||
CASE("remove/not-in-any-queue", test_remove_invalid, 0), CASE("remove/already-removed", test_remove_invalid, 1), |
||||
CASE("remove/in-another-queue", test_remove_invalid, 2), |
||||
CASE("consumer/immediate", test_consume, 0), CASE("consumer/deferred", test_consume, 1), |
||||
CASE("consumer/suspend-resume", test_consume, 2), CASE("consumer/deferred-suspend-resume", test_consume, 3), |
||||
CASE("consumer/free-pending", test_consume_free_pending, 0), CASE("consumer/cancel-pending", test_consume_free_pending, 1), |
||||
CASE("waiter/cancel-free-queue", test_waiter_cancel, 0), CASE("waiter/cancel-busy-queue", test_waiter_cancel, 1), |
||||
CASE("waiter/cancel-released-queue", test_waiter_cancel, 2), |
||||
CASE("waiter/byte-threshold-immediate", test_waiter_bytes, 0), CASE("waiter/byte-threshold-deferred", test_waiter_bytes, 1), |
||||
CASE("waiter/teardown-free", test_waiter_teardown, 0), CASE("waiter/teardown-busy", test_waiter_teardown, 1), |
||||
CASE("waiter/teardown-released", test_waiter_teardown, 2), |
||||
CASE("waiter/auto-free-pending", test_waiter_auto_teardown, 0), |
||||
CASE("waiter/auto-free-busy", test_waiter_auto_teardown, 1), |
||||
CASE("waiter/auto-free-released", test_waiter_auto_teardown, 2), |
||||
CASE("waiter/rearm-pending", test_waiter_rearm, 0), CASE("waiter/rearm-busy", test_waiter_rearm, 1), |
||||
CASE("callback/free-put", test_callback_free_queue, 0), CASE("callback/free-put-first", test_callback_free_queue, 1), |
||||
CASE("callback/free-put-index", test_callback_free_queue, 2), CASE("callback/free-put-first-index", test_callback_free_queue, 3), |
||||
CASE("callback/free-deferred-put", test_callback_free_queue, 4), |
||||
CASE("callback/free-deferred-put-first", test_callback_free_queue, 5), |
||||
CASE("callback/free-deferred-put-index", test_callback_free_queue, 6), |
||||
CASE("callback/free-deferred-put-first-index", test_callback_free_queue, 7), |
||||
CASE("callback/free-empty", test_callback_free_queue, 8), CASE("callback/free-on-get", test_callback_free_queue, 9), |
||||
CASE("callback/free-waiter", test_callback_free_queue, 10), CASE("callback/free-deferred-waiter", test_callback_free_queue, 11), |
||||
CASE("model/no-index-10000", test_model, 0), CASE("model/collisions-10000", test_model, 1), |
||||
CASE("model/hash-10000", test_model, 2), |
||||
CASE("bytes/snapshot-get", test_snapshot_bytes, 0), CASE("bytes/snapshot-remove", test_snapshot_bytes, 1), |
||||
CASE("index/zero-put", test_index_bounds, 0), CASE("index/zero-put-first", test_index_bounds, 1), |
||||
CASE("index/overflow-put", test_index_bounds, 2), CASE("index/overflow-put-first", test_index_bounds, 3), |
||||
CASE("index/binary-offset-put", test_binary_index, 0), CASE("index/binary-offset-put-first", test_binary_index, 1), |
||||
CASE("ownership/dgram-malloc", test_dgram_ownership, 0), CASE("ownership/dgram-pool", test_dgram_ownership, 1), |
||||
CASE("ownership/dgram-custom", test_dgram_ownership, 2), |
||||
CASE("oom/waiter-immediate-dispatch-retry", test_waiter_schedule_oom, 0), |
||||
CASE("oom/waiter-released-dispatch-retry", test_waiter_schedule_oom, 1), |
||||
CASE("oom/queue-object", test_creation_oom, 1), CASE("oom/hash-table", test_creation_oom, 2), |
||||
CASE("oom/entry", test_entry_oom, 0), CASE("oom/dgram", test_entry_oom, 1), CASE("oom/waiter-retry", test_waiter_oom, 0), |
||||
}; |
||||
return regression_run(argc, argv, cases, sizeof(cases) / sizeof(cases[0])); |
||||
} |
||||
@ -0,0 +1,95 @@
|
||||
/* Проверяем лог до намеренной остановки; зависший child завершает parent. */ |
||||
#include "../lib/memory_pool.h" |
||||
#include "../lib/u_async.h" |
||||
#include "../lib/debug_config.h" |
||||
#include <assert.h> |
||||
#include <signal.h> |
||||
#include <poll.h> |
||||
#include <stdio.h> |
||||
#include <string.h> |
||||
#include <sys/wait.h> |
||||
#include <unistd.h> |
||||
|
||||
static int log_fd; |
||||
static struct UASYNC* child_ua; |
||||
static void* child_timer; |
||||
|
||||
static void capture_log(int level, const char* category, const char* message) { |
||||
(void)level; (void)category; |
||||
size_t len = strlen(message); |
||||
assert(write(log_fd, message, len) == (ssize_t)len); |
||||
} |
||||
|
||||
static void free_executing_timer(void* arg) { |
||||
(void)arg; |
||||
memory_pool_free_impl(child_ua->timeout_pool, child_timer, "first free inside callback"); |
||||
} |
||||
|
||||
static void damage(int mode) { |
||||
if (mode == 2) { |
||||
child_ua = uasync_create(); assert(child_ua); |
||||
child_timer = uasync_set_timeout(child_ua, 0, NULL, free_executing_timer, "corrupt_timer_test"); assert(child_timer); |
||||
for (int i = 0; i < 10; i++) uasync_poll(child_ua, 10); |
||||
} else { |
||||
struct memory_pool* pool = memory_pool_init(16, "corrupt_pool_test"); assert(pool); |
||||
void* obj = memory_pool_alloc_impl(pool, "allocation test location"); assert(obj); |
||||
if (mode == 0) { |
||||
memory_pool_free_impl(pool, obj, "first free test location"); |
||||
memory_pool_free_impl(pool, obj, "repeated free test location"); |
||||
} else { |
||||
/* Битые pointers в хвосте нельзя разыменовывать для вывода строки. */ |
||||
memset((char*)obj + pool->object_size, 0xa5, 4 + 2 * sizeof(const char*)); |
||||
memory_pool_free_impl(pool, obj, "overflow free test location"); |
||||
} |
||||
} |
||||
_exit(1); /* Остановка обязательна. */ |
||||
} |
||||
|
||||
static void check_log_before_halt(int mode) { |
||||
int pipe_fd[2]; assert(pipe(pipe_fd) == 0); |
||||
pid_t pid = fork(); assert(pid >= 0); |
||||
if (!pid) { |
||||
close(pipe_fd[0]); log_fd = pipe_fd[1]; |
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_enable_console(0); |
||||
debug_set_log_hook(capture_log); |
||||
damage(mode); |
||||
} |
||||
close(pipe_fd[1]); |
||||
char output[8192] = {0}; size_t used = 0; |
||||
uint64_t deadline = get_time_tb() + 30000; |
||||
while (!strstr(output, "stopping thread permanently") && get_time_tb() < deadline) { |
||||
struct pollfd p = { .fd = pipe_fd[0], .events = POLLIN }; |
||||
if (poll(&p, 1, 100) <= 0) continue; |
||||
ssize_t len = read(pipe_fd[0], output + used, sizeof(output) - used - 1); |
||||
if (len <= 0) break; |
||||
used += len; output[used] = 0; |
||||
if (used == sizeof(output) - 1) break; |
||||
} |
||||
/* Если вместо остановки произошёл abort/exit, pipe получит HUP. */ |
||||
struct pollfd stopped = { .fd = pipe_fd[0], .events = POLLIN }; |
||||
poll(&stopped, 1, 100); |
||||
int status = 0; |
||||
int still_running = waitpid(pid, &status, WNOHANG) == 0; |
||||
if (still_running) { assert(kill(pid, SIGKILL) == 0); assert(waitpid(pid, &status, 0) == pid); } |
||||
close(pipe_fd[0]); |
||||
fprintf(stderr, "%s", output); |
||||
assert(still_running && strstr(output, "stopping thread permanently")); |
||||
if (mode == 1) { |
||||
assert(strstr(output, "BUFFER OVERFLOW") && strstr(output, "overflow free test location")); |
||||
} else { |
||||
assert(strstr(output, "DOUBLE FREE") && strstr(output, "first_free=") && strstr(output, "repeated_free=")); |
||||
if (mode == 0) { |
||||
assert(strstr(output, "allocation test location") && strstr(output, "first free test location")); |
||||
assert(strstr(output, "repeated free test location")); |
||||
} else { |
||||
assert(strstr(output, "timer freed inside callback") && strstr(output, "corrupt_timer_test")); |
||||
assert(strstr(output, "first free inside callback")); |
||||
} |
||||
} |
||||
} |
||||
|
||||
int main(void) { |
||||
for (int i = 0; i < 3; i++) check_log_before_halt(i); |
||||
puts("PASS: double free, damaged canary/metadata and timer callback log before permanent halt"); |
||||
return 0; |
||||
} |
||||
@ -0,0 +1,551 @@
|
||||
/* Контракты event loop: обе ветки опроса, жизненный цикл, fault injection. */ |
||||
#define _GNU_SOURCE |
||||
#include "async_regression.h" |
||||
#include <errno.h> |
||||
#include <fcntl.h> |
||||
#include <pthread.h> |
||||
#include <string.h> |
||||
#include <sys/socket.h> |
||||
#include <unistd.h> |
||||
|
||||
struct socket_test { |
||||
struct UASYNC* ua; |
||||
void* id; |
||||
int fd[2]; |
||||
int reads, writes, errors, callback_error, action; |
||||
}; |
||||
|
||||
static void count_cb(void* arg) { ++*(int*)arg; } |
||||
static void read_cb(int fd, void* arg) { |
||||
struct socket_test* t = arg; |
||||
char byte; |
||||
if (read(fd, &byte, 1) < 0 && errno != EAGAIN) t->callback_error = 1; |
||||
t->reads++; |
||||
if (t->action == 1) { |
||||
if (uasync_remove_socket(t->ua, t->id) != ERR_OK) t->callback_error = 1; |
||||
t->id = NULL; |
||||
} else if (t->action == 2) { |
||||
if (uasync_set_socket_write(t->ua, t->id, 0) != ERR_OK) t->callback_error = 1; |
||||
} |
||||
} |
||||
static void write_cb(int fd, void* arg) { (void)fd; ((struct socket_test*)arg)->writes++; } |
||||
static void error_cb(int fd, void* arg) { (void)fd; ((struct socket_test*)arg)->errors++; } |
||||
|
||||
static int socket_setup(struct socket_test* t, int param, int want_read, int want_write, int want_error) { |
||||
memset(t, 0, sizeof(*t)); |
||||
if (param & 1) regression_epoll_create_fail(); |
||||
t->ua = uasync_create(); CHECK(t->ua); |
||||
CHECK_EQ(t->ua->use_epoll, !(param & 1)); |
||||
CHECK_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, t->fd), 0); |
||||
CHECK_EQ(socket_set_nonblocking(t->fd[0]), 0); |
||||
if (param & 2) { |
||||
t->id = uasync_add_socket_t(t->ua, t->fd[0], want_read ? read_cb : NULL, |
||||
want_write ? write_cb : NULL, want_error ? error_cb : NULL, "regression", t); |
||||
} else { |
||||
t->id = uasync_add_socket(t->ua, t->fd[0], want_read ? read_cb : NULL, |
||||
want_write ? write_cb : NULL, want_error ? error_cb : NULL, "regression", t); |
||||
} |
||||
CHECK(t->id); |
||||
return 0; |
||||
} |
||||
static void socket_cleanup(struct socket_test* t) { |
||||
if (t->id) uasync_remove_socket(t->ua, t->id); |
||||
close(t->fd[0]); if (t->fd[1] >= 0) close(t->fd[1]); |
||||
uasync_destroy(t->ua, 0); |
||||
} |
||||
|
||||
static int test_socket_read_write(int param) { |
||||
struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 1, 1), 0); |
||||
CHECK_EQ(write(t.fd[1], "x", 1), 1); |
||||
uasync_poll(t.ua, 0); |
||||
CHECK_EQ(t.reads, 1); CHECK_EQ(t.writes, 1); CHECK_EQ(t.errors, 0); CHECK_EQ(t.callback_error, 0); |
||||
socket_cleanup(&t); return 0; |
||||
} |
||||
static int test_remove_in_read(int param) { |
||||
struct socket_test t; CHECK_EQ(socket_setup(&t, param & 3, 1, !(param & 4), 1), 0); |
||||
t.action = 1; |
||||
CHECK_EQ(write(t.fd[1], "x", 1), 1); |
||||
if (param & 4) { close(t.fd[1]); t.fd[1] = -1; } |
||||
uasync_poll(t.ua, 0); |
||||
CHECK_EQ(t.reads, 1); CHECK_EQ(t.writes, 0); CHECK_EQ(t.errors, 0); CHECK_EQ(t.callback_error, 0); |
||||
socket_cleanup(&t); return 0; |
||||
} |
||||
static int test_disable_in_read(int param) { |
||||
struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 1, 0), 0); |
||||
t.action = 2; CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); |
||||
CHECK_EQ(t.reads, 1); CHECK_EQ(t.writes, 0); CHECK_EQ(t.callback_error, 0); |
||||
socket_cleanup(&t); return 0; |
||||
} |
||||
static int test_socket_toggle(int param) { |
||||
struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 1, 0), 0); |
||||
CHECK_EQ(uasync_set_socket_read(t.ua, t.id, 0), ERR_OK); |
||||
CHECK_EQ(uasync_set_socket_write(t.ua, t.id, 0), ERR_OK); |
||||
CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); |
||||
CHECK_EQ(t.reads, 0); CHECK_EQ(t.writes, 0); |
||||
CHECK_EQ(uasync_set_socket_read(t.ua, t.id, 1), ERR_OK); uasync_poll(t.ua, 0); |
||||
CHECK_EQ(t.reads, 1); CHECK_EQ(t.writes, 0); |
||||
CHECK_EQ(uasync_set_socket_write(t.ua, t.id, 1), ERR_OK); uasync_poll(t.ua, 0); |
||||
CHECK_EQ(t.writes, 1); |
||||
socket_cleanup(&t); return 0; |
||||
} |
||||
static int test_fd_mapping(int param) { |
||||
struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 0, 0), 0); |
||||
CHECK_EQ(uasync_remove_socket(t.ua, t.id), ERR_OK); t.id = NULL; |
||||
void* other = uasync_add_socket_t(t.ua, t.fd[1], read_cb, NULL, NULL, "other fd", &t); CHECK(other); |
||||
if (param & 4) { |
||||
CHECK_EQ(uasync_remove_socket_t(t.ua, t.fd[0]), ERR_FAIL); |
||||
CHECK_EQ(uasync_set_socket_read(t.ua, other, 0), ERR_OK); |
||||
} else { |
||||
t.id = uasync_add_socket(t.ua, t.fd[0], read_cb, NULL, NULL, "readded fd", &t); CHECK(t.id); |
||||
} |
||||
CHECK_EQ(uasync_remove_socket(t.ua, other), ERR_OK); |
||||
socket_cleanup(&t); return 0; |
||||
} |
||||
static int test_lookup(int param) { |
||||
struct socket_test t; CHECK_EQ(socket_setup(&t, param & 3, 1, 0, 0), 0); |
||||
if (param & 4) { |
||||
CHECK_EQ(uasync_remove_socket(t.ua, t.id), ERR_OK); t.id = NULL; |
||||
int high = fcntl(t.fd[0], F_DUPFD, FD_SETSIZE + 16); CHECK(high >= FD_SETSIZE); |
||||
close(t.fd[0]); t.fd[0] = high; |
||||
t.id = uasync_add_socket(t.ua, high, read_cb, NULL, NULL, "high fd", &t); CHECK(t.id); |
||||
} |
||||
void* found = NULL; CHECK_EQ(uasync_lookup_socket(t.ua, t.fd[0], &found), 0); |
||||
CHECK(found == t.id); |
||||
CHECK_EQ(uasync_set_socket_read(t.ua, found, 0), ERR_OK); |
||||
CHECK_EQ(uasync_set_socket_write(t.ua, found, 0), ERR_OK); |
||||
CHECK_EQ(uasync_remove_socket(t.ua, found), ERR_OK); t.id = NULL; |
||||
socket_cleanup(&t); return 0; |
||||
} |
||||
static int test_array_growth(int param) { |
||||
struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 0, 0), 0); |
||||
int high = fcntl(t.fd[0], F_DUPFD, 128); CHECK(high >= 128); |
||||
void* h = uasync_add_socket(t.ua, high, NULL, NULL, NULL, "grow", NULL); CHECK(h); |
||||
CHECK_EQ(uasync_remove_socket(t.ua, h), ERR_OK); close(high); |
||||
CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); CHECK_EQ(t.reads, 1); |
||||
CHECK_EQ(uasync_set_socket_read(t.ua, t.id, 0), ERR_OK); |
||||
socket_cleanup(&t); return 0; |
||||
} |
||||
static int test_duplicate_fd(int param) { |
||||
struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 0, 0), 0); |
||||
CHECK(!uasync_add_socket_t(t.ua, t.fd[0], read_cb, NULL, NULL, "duplicate", &t)); |
||||
CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); CHECK_EQ(t.reads, 1); |
||||
socket_cleanup(&t); return 0; |
||||
} |
||||
static int test_stale_batch(int param) { |
||||
(void)param; |
||||
struct socket_test t; CHECK_EQ(socket_setup(&t, 0, 0, 1, 0), 0); |
||||
struct epoll_event event; |
||||
CHECK_EQ(epoll_wait(t.ua->epoll_fd, &event, 1, 0), 1); |
||||
CHECK_EQ(uasync_remove_socket(t.ua, t.id), ERR_OK); |
||||
t.id = uasync_add_socket(t.ua, t.fd[0], NULL, write_cb, NULL, "new generation", &t); CHECK(t.id); |
||||
regression_epoll_events(&event, 1); uasync_poll(t.ua, 0); CHECK_EQ(t.writes, 0); |
||||
uasync_poll(t.ua, 0); CHECK_EQ(t.writes, 1); |
||||
socket_cleanup(&t); return 0; |
||||
} |
||||
struct replace_test { struct UASYNC* ua; int fd[4]; void* id[2]; int old_calls, new_calls, error; }; |
||||
static void replacement_cb(int fd, void* arg) { (void)fd; ((struct replace_test*)arg)->new_calls++; } |
||||
static void replace_other_cb(int fd, void* arg) { |
||||
struct replace_test* t = arg; |
||||
if (++t->old_calls != 1) return; |
||||
int other = fd == t->fd[0] ? 1 : 0; |
||||
if (uasync_remove_socket(t->ua, t->id[other]) != ERR_OK) t->error = 1; |
||||
t->id[other] = uasync_add_socket(t->ua, t->fd[other * 2], NULL, replacement_cb, NULL, "replacement", t); |
||||
if (!t->id[other]) t->error = 1; |
||||
} |
||||
static int test_replace_other_in_batch(int param) { |
||||
struct replace_test t = {0}; |
||||
if (param) regression_epoll_create_fail(); |
||||
t.ua = uasync_create(); CHECK(t.ua); |
||||
for (int i = 0; i < 2; i++) { |
||||
CHECK_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, t.fd + i * 2), 0); |
||||
t.id[i] = uasync_add_socket(t.ua, t.fd[i * 2], NULL, replace_other_cb, NULL, "original", &t); CHECK(t.id[i]); |
||||
} |
||||
uasync_poll(t.ua, 0); CHECK_EQ(t.error, 0); CHECK_EQ(t.old_calls, 1); CHECK_EQ(t.new_calls, 0); |
||||
uasync_poll(t.ua, 0); CHECK_EQ(t.new_calls, 1); |
||||
for (int i = 0; i < 2; i++) CHECK_EQ(uasync_remove_socket(t.ua, t.id[i]), ERR_OK); |
||||
for (int i = 0; i < 4; i++) close(t.fd[i]); |
||||
uasync_destroy(t.ua, 0); return 0; |
||||
} |
||||
|
||||
static int test_priority_event(int param) { |
||||
(void)param; |
||||
struct socket_test t; CHECK_EQ(socket_setup(&t, 0, 0, 1, 1), 0); |
||||
struct epoll_event event; CHECK_EQ(epoll_wait(t.ua->epoll_fd, &event, 1, 0), 1); |
||||
event.events = EPOLLPRI; regression_epoll_events(&event, 1); uasync_poll(t.ua, 0); |
||||
CHECK_EQ(t.errors, 1); |
||||
socket_cleanup(&t); return 0; |
||||
} |
||||
|
||||
struct free_context { struct UASYNC* ua; void* id; int* writes; }; |
||||
static void free_read_cb(int fd, void* arg) { |
||||
struct free_context* c = arg; (void)fd; |
||||
uasync_remove_socket(c->ua, c->id); u_free(c); |
||||
} |
||||
static void freed_write_cb(int fd, void* arg) { (void)fd; ++*((struct free_context*)arg)->writes; } |
||||
static int test_freed_context(int param) { |
||||
(void)param; |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
int fd[2], writes = 0; CHECK_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, fd), 0); |
||||
struct free_context* c = u_calloc(1, sizeof(*c)); CHECK(c); c->ua = ua; c->writes = &writes; |
||||
c->id = uasync_add_socket(ua, fd[0], free_read_cb, freed_write_cb, NULL, "free context", c); CHECK(c->id); |
||||
CHECK_EQ(write(fd[1], "x", 1), 1); uasync_poll(ua, 0); CHECK_EQ(writes, 0); |
||||
close(fd[0]); close(fd[1]); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
|
||||
static int test_due_timer(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); int count = 0; |
||||
CHECK(uasync_set_timeout(ua, 0, &count, count_cb, "due")); |
||||
if (param) regression_clock_shift(-60000); |
||||
uint64_t start = regression_time_ms(); uasync_poll(ua, param ? 0 : 10000); |
||||
uint64_t elapsed = regression_time_ms() - start; |
||||
regression_clock_shift(0); |
||||
fprintf(stderr, "[OBSERVE] due timer: elapsed=%llums callbacks=%d\n", (unsigned long long)elapsed, count); |
||||
CHECK_EQ(count, 1); CHECK(elapsed < 500); |
||||
uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_forward_clock(int param) { |
||||
(void)param; |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); int count = 0; |
||||
void* id = uasync_set_timeout(ua, 100000, &count, count_cb, "ten seconds"); CHECK(id); |
||||
regression_clock_shift(60000); uasync_poll(ua, 0); regression_clock_shift(0); |
||||
CHECK_EQ(count, 0); CHECK_EQ(uasync_cancel_timeout(ua, id), ERR_OK); |
||||
uasync_destroy(ua, 0); return 0; |
||||
} |
||||
struct timer_item { struct UASYNC* ua; void* id; int* count; int* sequence; int value; }; |
||||
static void timer_cb(void* arg) { |
||||
struct timer_item* t = arg; t->id = NULL; |
||||
if (t->sequence) t->sequence[*t->count] = t->value; |
||||
++*t->count; |
||||
} |
||||
static int test_timer_cancel(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct timer_item items[96] = {0}; int count = 0; |
||||
for (int i = 0; i < 96; i++) { |
||||
items[i].ua = ua; items[i].count = &count; |
||||
items[i].id = uasync_set_timeout(ua, param ? 100000 : 0, &items[i], timer_cb, "cancel heap growth"); |
||||
CHECK(items[i].id); |
||||
} |
||||
for (int i = 0; i < 96; i += 2) { |
||||
CHECK_EQ(uasync_cancel_timeout(ua, items[i].id), ERR_OK); items[i].id = NULL; |
||||
} |
||||
uasync_poll(ua, 0); CHECK_EQ(count, param ? 0 : 48); |
||||
for (int i = 0; i < 96; i++) if (items[i].id) { |
||||
CHECK_EQ(uasync_cancel_timeout(ua, items[i].id), ERR_OK); items[i].id = NULL; |
||||
} |
||||
uasync_poll(ua, 0); |
||||
size_t alloc, freed; uasync_get_stats(ua, &alloc, &freed, NULL, NULL); CHECK_EQ(alloc, freed); |
||||
uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_soon_fifo(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct timer_item items[96] = {0}; int sequence[96], count = 0; |
||||
for (int i = 0; i < 96; i++) { |
||||
items[i].value = i; items[i].count = &count; items[i].sequence = sequence; |
||||
items[i].id = uasync_call_soon(ua, &items[i], timer_cb); CHECK(items[i].id); |
||||
} |
||||
if (param) for (int i = 0; i < 96; i += 2) { |
||||
CHECK_EQ(uasync_call_soon_cancel(ua, items[i].id), ERR_OK); items[i].id = NULL; |
||||
} |
||||
uasync_drain_immediate(ua); CHECK_EQ(count, param ? 48 : 96); |
||||
for (int i = 0; i < count; i++) CHECK_EQ(sequence[i], param ? i * 2 + 1 : i); |
||||
uasync_destroy(ua, 0); return 0; |
||||
} |
||||
struct chain_test { struct UASYNC* ua; int runs, timer_runs, observed; }; |
||||
static void chain_cb(void* arg) { |
||||
struct chain_test* t = arg; |
||||
if (++t->runs == 1000) t->observed = t->timer_runs; |
||||
if (t->runs < 1000) uasync_call_soon(t->ua, t, chain_cb); |
||||
} |
||||
static int test_soon_fairness(int param) { |
||||
(void)param; |
||||
struct chain_test t = {0}; t.ua = uasync_create(); CHECK(t.ua); |
||||
CHECK(uasync_set_timeout(t.ua, 0, &t.timer_runs, count_cb, "fairness")); |
||||
CHECK(uasync_call_soon(t.ua, &t, chain_cb)); |
||||
for (int i = 0; i < 2000 && t.runs < 1000; i++) uasync_poll(t.ua, 0); |
||||
CHECK_EQ(t.runs, 1000); CHECK_EQ(t.observed, 1); |
||||
uasync_destroy(t.ua, 0); return 0; |
||||
} |
||||
static void stop_cb(void* arg) { uasync_stop(arg); } |
||||
static int test_stop_soon(int param) { |
||||
struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 0, 0), 0); |
||||
CHECK(uasync_call_soon(t.ua, t.ua, stop_cb)); uasync_mainloop(t.ua); |
||||
CHECK_EQ(t.ua->stop, 1); socket_cleanup(&t); return 0; |
||||
} |
||||
static int test_stopped_poll_cleanup(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); int calls = 0; |
||||
void* id = uasync_set_timeout(ua, 100000, &calls, count_cb, "cancel before stopped poll"); CHECK(id); |
||||
CHECK_EQ(uasync_cancel_timeout(ua, id), ERR_OK); |
||||
if (param) CHECK(uasync_call_soon(ua, ua, stop_cb)); else uasync_stop(ua); |
||||
uasync_poll(ua, -1); CHECK_EQ(calls, 0); CHECK_EQ(ua->timer_alloc_count, ua->timer_free_count); |
||||
uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_post_empty(int param) { |
||||
if (param) regression_epoll_create_fail(); |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); int count = 0; |
||||
uasync_post(ua, count_cb, &count); uasync_poll(ua, -1); CHECK_EQ(count, 1); |
||||
uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static void* worker_post(void* arg) { uasync_post(arg, stop_cb, arg); return NULL; } |
||||
static int test_worker_wakeup(int param) { |
||||
struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 0, 0), 0); |
||||
pthread_t worker; CHECK_EQ(pthread_create(&worker, NULL, worker_post, t.ua), 0); |
||||
uasync_mainloop(t.ua); CHECK_EQ(pthread_join(worker, NULL), 0); |
||||
CHECK_EQ(t.ua->stop, 1); socket_cleanup(&t); return 0; |
||||
} |
||||
static void* worker_stop(void* arg) { usleep(20000); uasync_stop(arg); return NULL; } |
||||
static int test_worker_stop(int param) { |
||||
if (param) regression_epoll_create_fail(); |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); pthread_t worker; |
||||
CHECK_EQ(pthread_create(&worker, NULL, worker_stop, ua), 0); |
||||
uasync_mainloop(ua); CHECK_EQ(pthread_join(worker, NULL), 0); |
||||
CHECK_EQ(ua->stop, 1); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_reserved_post_cancel(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); int count = 0; |
||||
struct posted_task* tasks[3]; |
||||
for (int i = 0; i < 3; i++) { |
||||
tasks[i] = u_calloc(1, sizeof(*tasks[i])); CHECK(tasks[i]); |
||||
tasks[i]->callback = count_cb; tasks[i]->arg = &count; uasync_post_reserved(ua, tasks[i]); |
||||
} |
||||
CHECK_EQ(uasync_cancel_post(ua, tasks[param]), ERR_OK); |
||||
uasync_poll(ua, 0); CHECK_EQ(count, 2); |
||||
uasync_destroy(ua, 0); return 0; |
||||
} |
||||
|
||||
static int test_socket_growth_oom(int param) { |
||||
struct socket_test t; CHECK_EQ(socket_setup(&t, 0, 1, 0, 0), 0); |
||||
int high = fcntl(t.fd[0], F_DUPFD, 128); CHECK(high >= 128); |
||||
regression_fault_arm(FAULT_ALLOC, "u_async.c:", param); |
||||
void* id = uasync_add_socket(t.ua, high, read_cb, NULL, NULL, "growth failure", &t); |
||||
CHECK(regression_fault_hit()); regression_fault_reset(); CHECK(!id); close(high); |
||||
CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); CHECK_EQ(t.reads, 1); |
||||
socket_cleanup(&t); return 0; |
||||
} |
||||
static int test_create_oom(int param) { |
||||
regression_fault_arm(FAULT_ALLOC, NULL, param); |
||||
struct UASYNC* ua = uasync_create(); |
||||
CHECK(regression_fault_hit()); regression_fault_reset(); CHECK(!ua); |
||||
return 0; |
||||
} |
||||
static int test_eventfd_failure(int param) { |
||||
(void)param; regression_eventfd_fail(); CHECK(!uasync_create()); return 0; |
||||
} |
||||
static int test_schedule_oom(int param) { |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); int count = 0; |
||||
regression_fault_arm(FAULT_CALLOC, "u_async.c:", 1); |
||||
void* id = param ? uasync_call_soon(ua, &count, count_cb) : uasync_set_timeout(ua, 0, &count, count_cb, "oom"); |
||||
CHECK(regression_fault_hit()); regression_fault_reset(); CHECK(!id); |
||||
uasync_poll(ua, 0); CHECK_EQ(count, 0); |
||||
CHECK(uasync_set_timeout(ua, 0, &count, count_cb, "recover")); uasync_poll(ua, 0); CHECK_EQ(count, 1); |
||||
uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_heap_growth_oom(int param) { |
||||
(void)param; |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); int count = 0; |
||||
void* ids[16]; |
||||
for (int i = 0; i < 16; i++) { ids[i] = uasync_set_timeout(ua, 100000, &count, count_cb, "heap"); CHECK(ids[i]); } |
||||
regression_fault_arm(FAULT_REALLOC, "timeout_heap.c:", 1); |
||||
CHECK(!uasync_set_timeout(ua, 0, &count, count_cb, "heap failure")); |
||||
CHECK(regression_fault_hit()); regression_fault_reset(); |
||||
for (int i = 0; i < 16; i++) CHECK_EQ(uasync_cancel_timeout(ua, ids[i]), ERR_OK); |
||||
uasync_poll(ua, 0); CHECK_EQ(count, 0); |
||||
uasync_destroy(ua, 0); return 0; |
||||
} |
||||
static int test_epoll_control_failure(int param) { |
||||
struct socket_test t; CHECK_EQ(socket_setup(&t, 0, 1, 1, 0), 0); |
||||
regression_epoll_ctl_fail(EPOLL_CTL_MOD); |
||||
int result = param ? uasync_set_socket_write(t.ua, t.id, 0) : uasync_set_socket_read(t.ua, t.id, 0); |
||||
CHECK_EQ(result, ERR_FAIL); |
||||
CHECK_EQ(param ? uasync_set_socket_write(t.ua, t.id, 0) : uasync_set_socket_read(t.ua, t.id, 0), ERR_OK); |
||||
socket_cleanup(&t); return 0; |
||||
} |
||||
static int test_epoll_add_failure(int param) { |
||||
(void)param; |
||||
struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
int fd[2]; CHECK_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, fd), 0); |
||||
regression_epoll_ctl_fail(EPOLL_CTL_ADD); |
||||
CHECK(!uasync_add_socket(ua, fd[0], read_cb, NULL, NULL, "failed add", NULL)); |
||||
void* id = uasync_add_socket(ua, fd[0], NULL, NULL, NULL, "retry add", NULL); CHECK(id); |
||||
CHECK_EQ(uasync_remove_socket(ua, id), ERR_OK); |
||||
close(fd[0]); close(fd[1]); uasync_destroy(ua, 0); return 0; |
||||
} |
||||
|
||||
/* Отказ памяти переносится из poll в create/add; готовые регистрации остаются рабочими. */ |
||||
static void* poll_add(struct UASYNC* ua, int fd, socket_callback_t read_fn, socket_callback_t write_fn, void* arg, int sock_api) { |
||||
if (sock_api) return uasync_add_socket_t(ua, fd, read_fn, write_fn, NULL, "poll reserve", arg); |
||||
return uasync_add_socket(ua, fd, read_fn, write_fn, NULL, "poll reserve", arg); |
||||
} |
||||
static int test_poll_create_oom(int param) { |
||||
regression_epoll_create_fail(); regression_fault_arm(FAULT_REALLOC, "u_async.c:", param); |
||||
struct UASYNC* ua = uasync_create(); |
||||
CHECK(regression_fault_hit()); regression_fault_reset(); CHECK(!ua); return 0; |
||||
} |
||||
static int test_poll_growth_oom(int param) { |
||||
struct socket_test t; CHECK_EQ(socket_setup(&t, 1, 1, 0, 0), 0); |
||||
CHECK(t.ua->poll_fds); CHECK_EQ(t.ua->poll_fds_capacity, 16); |
||||
int fds[15]; void* ids[15]; |
||||
for (int i = 0; i < 15; i++) { |
||||
fds[i] = dup(t.fd[0]); CHECK(fds[i] >= 0); |
||||
if (i < 14) { ids[i] = poll_add(t.ua, fds[i], NULL, NULL, NULL, param); CHECK(ids[i]); } |
||||
} |
||||
uasync_poll(t.ua, 0); CHECK_EQ(t.ua->poll_fds_count, 16); |
||||
struct pollfd* previous = t.ua->poll_fds; |
||||
size_t socket_alloc = t.ua->socket_alloc_count, socket_free = t.ua->socket_free_count; |
||||
regression_fault_arm(FAULT_REALLOC, "u_async.c:", 0); |
||||
for (int i = 0; i < 3; i++) CHECK(!poll_add(t.ua, fds[14], NULL, NULL, NULL, param)); |
||||
CHECK(regression_fault_hit()); CHECK(t.ua->poll_fds == previous); |
||||
CHECK_EQ(t.ua->poll_fds_count, 16); CHECK_EQ(t.ua->poll_fds_capacity, 16); |
||||
CHECK_EQ(t.ua->socket_alloc_count, socket_alloc); CHECK_EQ(t.ua->socket_free_count, socket_free); |
||||
void* found = NULL; CHECK_EQ(uasync_lookup_socket(t.ua, fds[14], &found), -1); CHECK(!found); |
||||
int timers = 0; CHECK(uasync_set_timeout(t.ua, 0, &timers, count_cb, "poll OOM timer")); |
||||
CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, -1); |
||||
CHECK_EQ(t.reads, 1); CHECK_EQ(timers, 1); |
||||
regression_fault_reset(); |
||||
ids[14] = poll_add(t.ua, fds[14], NULL, NULL, NULL, param); CHECK(ids[14]); |
||||
CHECK_EQ(t.ua->poll_fds_capacity, 32); |
||||
CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); CHECK_EQ(t.reads, 2); |
||||
for (int i = 0; i < 15; i++) { CHECK_EQ(uasync_remove_socket(t.ua, ids[i]), ERR_OK); close(fds[i]); } |
||||
socket_cleanup(&t); return 0; |
||||
} |
||||
static int test_poll_no_alloc(int param) { |
||||
struct socket_test t; CHECK_EQ(socket_setup(&t, param, 1, 1, 0), 0); |
||||
if (!param) CHECK(!t.ua->poll_fds); /* epoll не резервирует ненужный массив. */ |
||||
int timers = 0; CHECK(uasync_set_timeout(t.ua, 0, &timers, count_cb, "no allocation in poll")); |
||||
regression_fault_arm(FAULT_ALLOC, NULL, 0); |
||||
for (int i = 0; i < 8; i++) { |
||||
CHECK_EQ(uasync_set_socket_read(t.ua, t.id, i & 1), ERR_OK); |
||||
CHECK_EQ(uasync_set_socket_write(t.ua, t.id, i & 1), ERR_OK); |
||||
CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); |
||||
} |
||||
CHECK(!regression_fault_hit()); regression_fault_reset(); CHECK_EQ(timers, 1); |
||||
CHECK_EQ(t.reads, 4); CHECK_EQ(t.writes, 4); |
||||
socket_cleanup(&t); return 0; |
||||
} |
||||
struct poll_worker { struct UASYNC* ua; struct posted_task* task; }; |
||||
static void* poll_oom_worker(void* arg) { |
||||
struct poll_worker* t = arg; usleep(20000); |
||||
if (t->task) uasync_post_reserved(t->ua, t->task); else uasync_stop(t->ua); |
||||
return NULL; |
||||
} |
||||
static int test_poll_oom_wakeup(int param) { |
||||
regression_epoll_create_fail(); struct UASYNC* ua = uasync_create(); CHECK(ua); |
||||
struct poll_worker t = { .ua = ua }; |
||||
if (param) { t.task = u_calloc(1, sizeof(*t.task)); CHECK(t.task); t.task->callback = stop_cb; t.task->arg = ua; } |
||||
regression_fault_arm(FAULT_ALLOC, NULL, 0); pthread_t worker; |
||||
CHECK_EQ(pthread_create(&worker, NULL, poll_oom_worker, &t), 0); |
||||
uasync_mainloop(ua); CHECK_EQ(pthread_join(worker, NULL), 0); |
||||
CHECK_EQ(ua->stop, 1); CHECK(!regression_fault_hit()); regression_fault_reset(); |
||||
uasync_destroy(ua, 0); return 0; |
||||
} |
||||
struct poll_growth { |
||||
struct UASYNC* ua; int fd[2], extra[15]; void* id[16]; |
||||
int reads, writes, new_writes, callback_error, fail, sock_api, count_before; |
||||
}; |
||||
static void poll_old_write_cb(int fd, void* arg) { (void)fd; ((struct poll_growth*)arg)->writes++; } |
||||
static void poll_new_write_cb(int fd, void* arg) { (void)fd; ((struct poll_growth*)arg)->new_writes++; } |
||||
static void poll_growth_read_cb(int fd, void* arg) { |
||||
struct poll_growth* t = arg; char byte; |
||||
if (read(fd, &byte, 1) != 1) t->callback_error = 1; |
||||
if (t->reads++ != 0) return; |
||||
t->id[15] = poll_add(t->ua, t->extra[14], NULL, poll_new_write_cb, t, t->sock_api); |
||||
if (!!t->id[15] == t->fail || t->ua->poll_fds_count != t->count_before) t->callback_error = 1; |
||||
} |
||||
static int test_poll_growth_in_callback(int param) { |
||||
regression_epoll_create_fail(); struct poll_growth t = { .fail = param >= 2, .sock_api = param & 1 }; |
||||
t.ua = uasync_create(); CHECK(t.ua); CHECK_EQ(t.ua->poll_fds_capacity, 16); |
||||
CHECK_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, t.fd), 0); |
||||
t.id[0] = poll_add(t.ua, t.fd[0], poll_growth_read_cb, poll_old_write_cb, &t, t.sock_api); CHECK(t.id[0]); |
||||
for (int i = 0; i < 15; i++) { |
||||
t.extra[i] = i == 14 ? fcntl(t.fd[0], F_DUPFD, 128) : dup(t.fd[0]); CHECK(t.extra[i] >= 0); |
||||
if (i < 14) { |
||||
t.id[i + 1] = poll_add(t.ua, t.extra[i], NULL, i == 0 ? poll_old_write_cb : NULL, &t, t.sock_api); |
||||
CHECK(t.id[i + 1]); |
||||
} |
||||
} |
||||
t.count_before = 16; |
||||
if (t.fail) regression_fault_arm(param >= 4 ? FAULT_CALLOC : FAULT_REALLOC, "u_async.c:", param >= 4 ? param - 3 : 0); |
||||
CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); |
||||
CHECK_EQ(t.callback_error, 0); CHECK_EQ(t.reads, 1); CHECK_EQ(t.writes, 2); CHECK_EQ(t.new_writes, 0); |
||||
if (t.fail) { |
||||
CHECK(regression_fault_hit()); CHECK_EQ(t.ua->poll_fds_capacity, param >= 4 ? 32 : 16); regression_fault_reset(); |
||||
t.id[15] = poll_add(t.ua, t.extra[14], NULL, poll_new_write_cb, &t, t.sock_api); CHECK(t.id[15]); |
||||
} |
||||
CHECK_EQ(t.ua->poll_fds_capacity, 32); |
||||
CHECK_EQ(write(t.fd[1], "x", 1), 1); uasync_poll(t.ua, 0); |
||||
CHECK_EQ(t.callback_error, 0); CHECK_EQ(t.reads, 2); CHECK_EQ(t.writes, 4); CHECK_EQ(t.new_writes, 1); |
||||
for (int i = 0; i < 16; i++) CHECK_EQ(uasync_remove_socket(t.ua, t.id[i]), ERR_OK); |
||||
for (int i = 0; i < 15; i++) close(t.extra[i]); |
||||
close(t.fd[0]); close(t.fd[1]); uasync_destroy(t.ua, 0); return 0; |
||||
} |
||||
|
||||
#define CASE(name, fn, p) {name, fn, p} |
||||
#define SOCKET_MATRIX(name, fn) \ |
||||
CASE(name "/epoll/fd", fn, 0), CASE(name "/poll/fd", fn, 1), \
|
||||
CASE(name "/epoll/socket", fn, 2), CASE(name "/poll/socket", fn, 3) |
||||
int main(int argc, char** argv) { |
||||
const struct regression_case cases[] = { |
||||
SOCKET_MATRIX("socket/read-write", test_socket_read_write), |
||||
SOCKET_MATRIX("socket/remove-in-read", test_remove_in_read), |
||||
CASE("socket/remove-in-read-hup/epoll", test_remove_in_read, 4), |
||||
CASE("socket/remove-in-read-hup/poll", test_remove_in_read, 5), |
||||
SOCKET_MATRIX("socket/disable-write-in-read", test_disable_in_read), |
||||
SOCKET_MATRIX("socket/toggle", test_socket_toggle), |
||||
SOCKET_MATRIX("socket/fd-readd-after-slot-reuse", test_fd_mapping), |
||||
CASE("socket/remove-absent-fd/epoll", test_fd_mapping, 4), |
||||
CASE("socket/remove-absent-fd/poll", test_fd_mapping, 5), |
||||
SOCKET_MATRIX("socket/lookup-handle", test_lookup), |
||||
CASE("socket/lookup-high-fd/epoll", test_lookup, 4), |
||||
CASE("socket/lookup-high-fd/poll", test_lookup, 5), |
||||
SOCKET_MATRIX("socket/handle-after-array-growth", test_array_growth), |
||||
SOCKET_MATRIX("socket/duplicate-fd", test_duplicate_fd), |
||||
CASE("socket/replace-other-in-batch/epoll", test_replace_other_in_batch, 0), |
||||
CASE("socket/replace-other-in-batch/poll", test_replace_other_in_batch, 1), |
||||
CASE("socket/stale-epoll-generation", test_stale_batch, 0), |
||||
CASE("socket/epoll-priority-event", test_priority_event, 0), |
||||
CASE("socket/freed-context-in-read", test_freed_context, 0), |
||||
CASE("timer/due-before-long-poll", test_due_timer, 0), |
||||
CASE("timer/wall-clock-backwards", test_due_timer, 1), |
||||
CASE("timer/wall-clock-forwards", test_forward_clock, 0), |
||||
CASE("timer/cancel-expired-heap-growth", test_timer_cancel, 0), |
||||
CASE("timer/cancel-future-heap-growth", test_timer_cancel, 1), |
||||
CASE("soon/fifo", test_soon_fifo, 0), CASE("soon/fifo-with-cancel", test_soon_fifo, 1), |
||||
CASE("soon/timer-fairness", test_soon_fairness, 0), |
||||
CASE("stop/from-soon/epoll", test_stop_soon, 0), CASE("stop/from-soon/poll", test_stop_soon, 1), |
||||
CASE("stop/from-worker/epoll", test_worker_stop, 0), CASE("stop/from-worker/poll", test_worker_stop, 1), |
||||
CASE("stop/poll-cleans-cancelled-timers", test_stopped_poll_cleanup, 0), |
||||
CASE("stop/from-soon-cleans-cancelled-timers", test_stopped_poll_cleanup, 1), |
||||
CASE("post/empty/epoll", test_post_empty, 0), CASE("post/empty/poll", test_post_empty, 1), |
||||
CASE("post/worker-wakeup/epoll", test_worker_wakeup, 0), CASE("post/worker-wakeup/poll", test_worker_wakeup, 1), |
||||
CASE("post/cancel-head", test_reserved_post_cancel, 0), CASE("post/cancel-middle", test_reserved_post_cancel, 1), |
||||
CASE("post/cancel-tail", test_reserved_post_cancel, 2), |
||||
CASE("poll-reserve/create-oom-once", test_poll_create_oom, 1), |
||||
CASE("poll-reserve/create-oom-persistent", test_poll_create_oom, 0), |
||||
CASE("poll-reserve/add-oom-fd-recovery", test_poll_growth_oom, 0), |
||||
CASE("poll-reserve/add-oom-socket-recovery", test_poll_growth_oom, 1), |
||||
CASE("poll-reserve/no-alloc-in-epoll", test_poll_no_alloc, 0), |
||||
CASE("poll-reserve/no-alloc-in-poll", test_poll_no_alloc, 1), |
||||
CASE("poll-reserve/stop-during-oom", test_poll_oom_wakeup, 0), |
||||
CASE("poll-reserve/post-during-oom", test_poll_oom_wakeup, 1), |
||||
CASE("poll-reserve/callback-grow-fd", test_poll_growth_in_callback, 0), |
||||
CASE("poll-reserve/callback-grow-socket", test_poll_growth_in_callback, 1), |
||||
CASE("poll-reserve/callback-oom-fd", test_poll_growth_in_callback, 2), |
||||
CASE("poll-reserve/callback-oom-socket", test_poll_growth_in_callback, 3), |
||||
CASE("poll-reserve/callback-socket-array-oom-1", test_poll_growth_in_callback, 4), |
||||
CASE("poll-reserve/callback-socket-array-oom-2", test_poll_growth_in_callback, 5), |
||||
CASE("poll-reserve/callback-socket-array-oom-3", test_poll_growth_in_callback, 6), |
||||
CASE("poll-reserve/callback-socket-array-oom-4", test_poll_growth_in_callback, 7), |
||||
CASE("oom/socket-grow-1", test_socket_growth_oom, 1), CASE("oom/socket-grow-2", test_socket_growth_oom, 2), |
||||
CASE("oom/socket-grow-3", test_socket_growth_oom, 3), CASE("oom/socket-grow-4", test_socket_growth_oom, 4), |
||||
CASE("oom/create-1", test_create_oom, 1), CASE("oom/create-2", test_create_oom, 2), |
||||
CASE("oom/create-3", test_create_oom, 3), CASE("oom/create-4", test_create_oom, 4), |
||||
CASE("oom/create-5", test_create_oom, 5), CASE("oom/create-6", test_create_oom, 6), |
||||
CASE("oom/create-7", test_create_oom, 7), CASE("oom/create-8", test_create_oom, 8), CASE("oom/create-9", test_create_oom, 9), |
||||
CASE("oom/eventfd-create", test_eventfd_failure, 0), |
||||
CASE("oom/timer-node", test_schedule_oom, 0), CASE("oom/soon-node", test_schedule_oom, 1), |
||||
CASE("oom/heap-growth", test_heap_growth_oom, 0), |
||||
CASE("epoll/failed-add-retry", test_epoll_add_failure, 0), |
||||
CASE("epoll/failed-disable-read", test_epoll_control_failure, 0), |
||||
CASE("epoll/failed-disable-write", test_epoll_control_failure, 1), |
||||
}; |
||||
return regression_run(argc, argv, cases, sizeof(cases) / sizeof(cases[0])); |
||||
} |
||||
@ -1,137 +1,177 @@
|
||||
/*
|
||||
* android_udp_log.c — non-blocking UDP log sender for Android |
||||
* |
||||
* Called ONLY from uasync worker thread (bridge_debug_hook). |
||||
* No mutex needed. Flushes at 16KB threshold or via uasync timer. |
||||
*/ |
||||
/* Android UDP-лог: общий буфер под mutex; таймер принадлежит только uasync-потоку.
|
||||
* ULOG не проходит через debug hook, поэтому ошибки отправки не вызывают рекурсию. */ |
||||
#include "android_udp_log.h" |
||||
#include "../libutun_lite/instance_lite.h" |
||||
#include "../../../lib/u_async.h" |
||||
#include "../../../lib/platform_compat.h" |
||||
#include "../../../lib/socket_compat.h" |
||||
#include "../../../lib/mem.h" |
||||
#include <pthread.h> |
||||
#include <stdio.h> |
||||
#include <string.h> |
||||
#include <stdlib.h> |
||||
#include <errno.h> |
||||
|
||||
#define UDP_BUF_SIZE 65536 |
||||
#define UDP_BUF_SIZE 65507 /* Максимальный UDP payload для IPv4. */ |
||||
#define UDP_FLUSH_BYTES 16384 |
||||
#define UDP_FLUSH_TB 2000 /* 200ms in uasync timebase (0.1ms units) */ |
||||
#define UDP_FLUSH_TB 2000 |
||||
|
||||
#ifdef __ANDROID__ |
||||
#include <android/log.h> |
||||
#define ULOG(fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, "utun-udplog", fmt, ##__VA_ARGS__) |
||||
#define ULOG(fmt, ...) __android_log_print(ANDROID_LOG_INFO, "utun-udplog", fmt, ##__VA_ARGS__) |
||||
#else |
||||
#define ULOG(fmt, ...) fprintf(stderr, "utun-udplog: " fmt "\n", ##__VA_ARGS__) |
||||
#define ULOG(fmt, ...) fprintf(stderr, "utun-udplog: " fmt "\n", ##__VA_ARGS__) |
||||
#endif |
||||
|
||||
static int g_udp_fd = -1; |
||||
static pthread_mutex_t g_udp_mutex = PTHREAD_MUTEX_INITIALIZER; |
||||
static struct UASYNC* g_udp_ua; |
||||
static int g_udp_fd = -1; |
||||
static struct sockaddr_in g_udp_addr; |
||||
static int g_udp_has_addr = 0; |
||||
static int g_udp_stopping = 0; |
||||
static char g_udp_buf[UDP_BUF_SIZE]; |
||||
static int g_udp_buf_len = 0; |
||||
static void* g_udp_timer = NULL; |
||||
static int g_udp_sent_first = 0; |
||||
static int64_t g_udp_total_sent = 0; |
||||
static int g_udp_send_errors = 0; |
||||
|
||||
static void udp_reschedule_timer(void) { |
||||
if (g_udp_stopping) return; |
||||
if (g_udp_timer) return; |
||||
struct UASYNC* ua = instance_lite_get_uasync(); |
||||
if (ua) g_udp_timer = uasync_set_timeout(ua, UDP_FLUSH_TB, NULL, udp_trampoline, "udp_log_flush"); |
||||
} |
||||
|
||||
static void udp_open_socket(void) { |
||||
if (g_udp_fd >= 0 || !g_udp_has_addr) return; |
||||
g_udp_fd = (int)socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0); |
||||
if (g_udp_fd < 0) { ULOG("socket() failed: %s (errno=%d)", strerror(errno), errno); return; } |
||||
if (connect(g_udp_fd, (struct sockaddr*)&g_udp_addr, sizeof(g_udp_addr)) < 0) { |
||||
ULOG("connect() to %s:%d failed: %s (errno=%d)", |
||||
inet_ntoa(g_udp_addr.sin_addr), (int)ntohs(g_udp_addr.sin_port), strerror(errno), errno); |
||||
close(g_udp_fd); g_udp_fd = -1; return; |
||||
} |
||||
ULOG("socket opened fd=%d -> %s:%d", g_udp_fd, |
||||
inet_ntoa(g_udp_addr.sin_addr), (int)ntohs(g_udp_addr.sin_port)); |
||||
} |
||||
static int g_udp_has_addr; |
||||
static int g_udp_stopping; |
||||
static char g_udp_buf[UDP_BUF_SIZE]; |
||||
static int g_udp_buf_len; |
||||
static int g_udp_sent_first; |
||||
static int64_t g_udp_total_sent; |
||||
static unsigned g_udp_send_errors; |
||||
/* Только uasync-поток читает и меняет handle. JNI и log hook его не трогают. */ |
||||
static void* g_udp_timer; |
||||
|
||||
/* Вызывается под g_udp_mutex; все операции сокета неблокирующие. */ |
||||
static void udp_do_send(const char* data, int len) { |
||||
if (g_udp_fd < 0) { udp_open_socket(); if (g_udp_fd < 0) return; } |
||||
if (g_udp_fd < 0) { |
||||
g_udp_fd = (int)socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0); |
||||
if (g_udp_fd < 0) { ULOG("socket failed: %s (errno=%d)", strerror(errno), errno); return; } |
||||
if (connect(g_udp_fd, (struct sockaddr*)&g_udp_addr, sizeof(g_udp_addr)) < 0) { |
||||
ULOG("connect failed: %s (errno=%d)", strerror(errno), errno); |
||||
close(g_udp_fd); g_udp_fd = -1; return; |
||||
} |
||||
} |
||||
ssize_t sent = send(g_udp_fd, data, len, MSG_DONTWAIT); |
||||
if (sent >= 0) { |
||||
if (sent == len) { |
||||
g_udp_total_sent += sent; |
||||
if (!g_udp_sent_first) { ULOG("first send OK: %zd bytes, total=%lld", sent, (long long)g_udp_total_sent); g_udp_sent_first = 1; } |
||||
if (!g_udp_sent_first) { ULOG("first send OK: %zd bytes", sent); g_udp_sent_first = 1; } |
||||
return; |
||||
} |
||||
int send_errno = sent < 0 ? errno : 0; |
||||
g_udp_send_errors++; |
||||
if (g_udp_send_errors <= 3 || g_udp_send_errors % 100 == 0) |
||||
ULOG("send() error #%d: %s (errno=%d)", g_udp_send_errors, strerror(errno), errno); |
||||
if (errno == ECONNREFUSED || errno == ENETUNREACH || errno == EHOSTUNREACH) { |
||||
close(g_udp_fd); |
||||
g_udp_fd = -1; |
||||
ULOG("send error #%u: sent=%zd expected=%d error=%s (errno=%d)", |
||||
g_udp_send_errors, sent, len, send_errno ? strerror(send_errno) : "short datagram", send_errno); |
||||
if (send_errno == ECONNREFUSED || send_errno == ENETUNREACH || send_errno == EHOSTUNREACH) { |
||||
close(g_udp_fd); g_udp_fd = -1; |
||||
} |
||||
} |
||||
|
||||
static void udp_flush_locked(void) { |
||||
if (!g_udp_buf_len) return; |
||||
udp_do_send(g_udp_buf, g_udp_buf_len); |
||||
g_udp_buf_len = 0; |
||||
} |
||||
|
||||
static void udp_update_timer(void* arg); |
||||
|
||||
static void udp_flush_timer(void* arg) { |
||||
g_udp_timer = NULL; |
||||
pthread_mutex_lock(&g_udp_mutex); |
||||
if (g_udp_ua == arg && g_udp_has_addr && !g_udp_stopping) udp_flush_locked(); |
||||
pthread_mutex_unlock(&g_udp_mutex); |
||||
udp_update_timer(arg); |
||||
} |
||||
|
||||
/* Только uasync-поток: attach или posted-команда смены адреса. Mutex отпускаем
|
||||
* до операций с таймером: их DEBUG_ERROR может снова вызвать udp_log_write. */ |
||||
static void udp_update_timer(void* arg) { |
||||
struct UASYNC* ua = arg; |
||||
pthread_mutex_lock(&g_udp_mutex); |
||||
int owns = g_udp_ua == ua; |
||||
int enabled = g_udp_has_addr && !g_udp_stopping; |
||||
pthread_mutex_unlock(&g_udp_mutex); |
||||
if (!owns) return; |
||||
if (!enabled) { |
||||
if (g_udp_timer) uasync_cancel_timeout(ua, g_udp_timer); |
||||
g_udp_timer = NULL; |
||||
} else if (!g_udp_timer) { |
||||
g_udp_timer = uasync_set_timeout(ua, UDP_FLUSH_TB, ua, udp_flush_timer, "udp_log_flush"); |
||||
if (!g_udp_timer) ULOG("flush timer allocation failed ua=%p", (void*)ua); |
||||
} |
||||
} |
||||
|
||||
void udp_trampoline(void* arg) { |
||||
(void)arg; |
||||
if (g_udp_buf_len > 0) { |
||||
udp_do_send(g_udp_buf, g_udp_buf_len); |
||||
g_udp_buf_len = 0; |
||||
void udp_log_attach(struct UASYNC* ua) { |
||||
if (!ua) { ULOG("attach rejected: ua=NULL"); return; } |
||||
pthread_mutex_lock(&g_udp_mutex); |
||||
if (g_udp_ua) { |
||||
ULOG("attach rejected: owner=%p requested=%p", (void*)g_udp_ua, (void*)ua); |
||||
pthread_mutex_unlock(&g_udp_mutex); |
||||
return; |
||||
} |
||||
g_udp_ua = ua; |
||||
pthread_mutex_unlock(&g_udp_mutex); |
||||
udp_update_timer(ua); |
||||
ULOG("attached ua=%p: flush timer owned by core thread", (void*)ua); |
||||
} |
||||
|
||||
void udp_log_detach(struct UASYNC* ua) { |
||||
if (!ua) return; |
||||
pthread_mutex_lock(&g_udp_mutex); |
||||
if (g_udp_ua != ua) { pthread_mutex_unlock(&g_udp_mutex); return; } |
||||
g_udp_ua = NULL; /* После этого ни один producer не может post в старый ua. */ |
||||
pthread_mutex_unlock(&g_udp_mutex); |
||||
if (g_udp_timer) uasync_cancel_timeout(ua, g_udp_timer); |
||||
g_udp_timer = NULL; |
||||
/* перевзводим только если остались неотправленные данные */ |
||||
if (g_udp_buf_len > 0) udp_reschedule_timer(); |
||||
ULOG("detached ua=%p: flush timer cancelled", (void*)ua); |
||||
} |
||||
|
||||
void udp_log_set_target(const char* ip, int port) { |
||||
g_udp_buf_len = 0; |
||||
g_udp_stopping = 0; |
||||
pthread_mutex_lock(&g_udp_mutex); |
||||
if (g_udp_has_addr) udp_flush_locked(); |
||||
if (g_udp_fd >= 0) { close(g_udp_fd); g_udp_fd = -1; } |
||||
memset(&g_udp_addr, 0, sizeof(g_udp_addr)); |
||||
g_udp_addr.sin_family = AF_INET; |
||||
g_udp_has_addr = ip && port > 0 && port <= 65535 && inet_pton(AF_INET, ip, &g_udp_addr.sin_addr) == 1; |
||||
g_udp_addr.sin_port = htons((unsigned short)(g_udp_has_addr ? port : 0)); |
||||
g_udp_stopping = !g_udp_has_addr; |
||||
g_udp_sent_first = 0; |
||||
g_udp_total_sent = 0; |
||||
g_udp_send_errors = 0; |
||||
if (g_udp_fd >= 0) { close(g_udp_fd); g_udp_fd = -1; } |
||||
if (ip && port > 0) { |
||||
memset(&g_udp_addr, 0, sizeof(g_udp_addr)); |
||||
g_udp_addr.sin_family = AF_INET; |
||||
g_udp_addr.sin_port = htons((unsigned short)port); |
||||
g_udp_has_addr = (inet_pton(AF_INET, ip, &g_udp_addr.sin_addr) == 1); |
||||
ULOG("set target %s:%d -> has_addr=%d", ip, port, g_udp_has_addr); |
||||
} else { |
||||
g_udp_has_addr = 0; |
||||
ULOG("set target disabled (ip=%s port=%d)", ip ? ip : "null", port); |
||||
if (g_udp_ua) { |
||||
/* Маленький u_malloc не логирует; при отказе используем только ULOG.
|
||||
* post_reserved тоже не вызывает debug hook. Detach ждёт этот mutex. */ |
||||
struct posted_task* task = u_malloc(sizeof(*task)); |
||||
if (task) { |
||||
task->callback = udp_update_timer; |
||||
task->arg = g_udp_ua; |
||||
uasync_post_reserved(g_udp_ua, task); |
||||
} else ULOG("target changed but timer update allocation failed"); |
||||
} |
||||
ULOG("target %s:%d enabled=%d", ip ? ip : "(null)", port, g_udp_has_addr); |
||||
pthread_mutex_unlock(&g_udp_mutex); |
||||
} |
||||
|
||||
void udp_log_write(const char* data, int len) { |
||||
if (!data || len <= 0 || !g_udp_has_addr) return; |
||||
|
||||
if (g_udp_fd < 0) udp_open_socket(); |
||||
|
||||
if (g_udp_buf_len + len > UDP_BUF_SIZE) { |
||||
if (g_udp_buf_len > 0) { udp_do_send(g_udp_buf, g_udp_buf_len); g_udp_buf_len = 0; } |
||||
if (len > UDP_BUF_SIZE) return; |
||||
} |
||||
memcpy(g_udp_buf + g_udp_buf_len, data, len); |
||||
g_udp_buf_len += len; |
||||
if (g_udp_buf_len >= UDP_FLUSH_BYTES) { |
||||
udp_do_send(g_udp_buf, g_udp_buf_len); |
||||
g_udp_buf_len = 0; |
||||
if (!data || len <= 0) return; |
||||
pthread_mutex_lock(&g_udp_mutex); |
||||
if (!g_udp_has_addr || g_udp_stopping) { pthread_mutex_unlock(&g_udp_mutex); return; } |
||||
if (len > UDP_BUF_SIZE) { |
||||
ULOG("log dropped: %d bytes exceeds UDP payload limit %d", len, UDP_BUF_SIZE); |
||||
} else { |
||||
udp_reschedule_timer(); /* взвести flush для частичных данных */ |
||||
if (len > UDP_BUF_SIZE - g_udp_buf_len) udp_flush_locked(); |
||||
memcpy(g_udp_buf + g_udp_buf_len, data, len); |
||||
g_udp_buf_len += len; |
||||
if (g_udp_buf_len >= UDP_FLUSH_BYTES) udp_flush_locked(); |
||||
} |
||||
pthread_mutex_unlock(&g_udp_mutex); |
||||
} |
||||
|
||||
void udp_log_cancel_timer(void) { |
||||
g_udp_stopping = 1; |
||||
struct UASYNC* ua = instance_lite_get_uasync(); |
||||
if (ua && g_udp_timer) uasync_cancel_timeout(ua, g_udp_timer); |
||||
g_udp_timer = NULL; |
||||
void udp_log_flush(void) { |
||||
pthread_mutex_lock(&g_udp_mutex); |
||||
if (g_udp_has_addr) udp_flush_locked(); |
||||
pthread_mutex_unlock(&g_udp_mutex); |
||||
} |
||||
|
||||
void udp_log_stop(void) { |
||||
pthread_mutex_lock(&g_udp_mutex); |
||||
g_udp_stopping = 1; |
||||
if (g_udp_buf_len > 0) { udp_do_send(g_udp_buf, g_udp_buf_len); g_udp_buf_len = 0; } |
||||
ULOG("stop: total_sent=%lld bytes, errors=%d", (long long)g_udp_total_sent, g_udp_send_errors); |
||||
if (g_udp_has_addr) udp_flush_locked(); |
||||
ULOG("stop: total_sent=%lld bytes errors=%u", (long long)g_udp_total_sent, g_udp_send_errors); |
||||
if (g_udp_fd >= 0) { close(g_udp_fd); g_udp_fd = -1; } |
||||
g_udp_has_addr = 0; |
||||
pthread_mutex_unlock(&g_udp_mutex); |
||||
} |
||||
|
||||
@ -0,0 +1,96 @@
|
||||
#include "audio_device.h" |
||||
#include "../transport/audio_diagnostics.h" |
||||
#include "debug_config.h" |
||||
#include <algorithm> |
||||
#include <utility> |
||||
|
||||
AudioDevice::AudioDevice() { m_clock.start(); } |
||||
AudioDevice::~AudioDevice() { close(); } |
||||
|
||||
bool AudioDevice::init(ma_context* context, ma_device_config config, const char* role, uint64_t session, Callback callback) { |
||||
close(); |
||||
m_role = role; |
||||
m_callback = std::move(callback); |
||||
if (!context) { retryLater(); return false; } |
||||
auto* device = new ma_device{}; |
||||
config.dataCallback = data; |
||||
config.notificationCallback = notification; |
||||
config.pUserData = this; |
||||
ma_result result = ma_device_init(context, &config, device); |
||||
if (result != MA_SUCCESS) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: %s init failed result=%d", role, result); |
||||
delete device; |
||||
retryLater(); |
||||
return false; |
||||
} |
||||
m_device = device; |
||||
m_frames = 0; |
||||
m_seenFrames = 0; |
||||
m_suspended = false; |
||||
audio_diag_label(device, session, role); |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: %s initialized backend=%s rate=%u capture='%s' playback='%s' period=%u/%u", |
||||
role, ma_get_backend_name(context->backend), device->sampleRate, device->capture.name, device->playback.name, |
||||
device->capture.internalPeriodSizeInFrames, device->playback.internalPeriodSizeInFrames); |
||||
return true; |
||||
} |
||||
|
||||
bool AudioDevice::start() { |
||||
if (!m_device) return false; |
||||
m_progressAt = m_clock.elapsed(); |
||||
ma_result result = ma_device_start(m_device); |
||||
if (result != MA_SUCCESS) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: %s start failed result=%d", m_role, result); |
||||
close(); |
||||
retryLater(); |
||||
return false; |
||||
} |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: %s started", m_role); |
||||
return true; |
||||
} |
||||
|
||||
void AudioDevice::close() { |
||||
if (m_device) { |
||||
ma_device_uninit(m_device); /* uninit включает stop/join, в том числе после неудачного start. */ |
||||
delete m_device; |
||||
m_device = nullptr; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: %s closed", m_role); |
||||
} |
||||
m_callback = {}; |
||||
} |
||||
|
||||
void AudioDevice::data(ma_device* device, void* out, const void* in, ma_uint32 frames) { |
||||
auto* self = static_cast<AudioDevice*>(device->pUserData); |
||||
if (self->m_callback) self->m_callback(in, out, frames); |
||||
self->m_frames.fetch_add(frames, std::memory_order_relaxed); |
||||
} |
||||
|
||||
void AudioDevice::notification(const ma_device_notification* event) { |
||||
auto* self = static_cast<AudioDevice*>(event->pDevice->pUserData); |
||||
if (event->type == ma_device_notification_type_interruption_began) self->m_suspended = true; |
||||
if (event->type == ma_device_notification_type_interruption_ended) self->m_suspended = false; |
||||
} |
||||
|
||||
bool AudioDevice::needsRecovery() { |
||||
if (!m_device) return false; |
||||
uint64_t frames = m_frames.load(std::memory_order_relaxed); |
||||
qint64 now = m_clock.elapsed(); |
||||
if (frames != m_seenFrames || m_suspended.load()) { |
||||
m_seenFrames = frames; |
||||
m_progressAt = now; |
||||
if (frames) m_retryDelay = 250; |
||||
} |
||||
bool stopped = ma_device_get_state(m_device) == ma_device_state_stopped; |
||||
if (!stopped && now - m_progressAt < 1000) return false; |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: %s recovery reason=%s idle=%lldms frames=%llu suspended=%d", |
||||
m_role, stopped ? "stopped" : "no-progress", (long long)(now - m_progressAt), |
||||
(unsigned long long)frames, m_suspended.load()); |
||||
return true; |
||||
} |
||||
|
||||
bool AudioDevice::retryReady() const { return !m_device && m_clock.elapsed() >= m_retryAt; } |
||||
void AudioDevice::retryLater() { |
||||
m_retryAt = m_clock.elapsed() + m_retryDelay; |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: %s retry in %dms", m_role, m_retryDelay); |
||||
m_retryDelay = std::min(m_retryDelay * 2, 2000); |
||||
} |
||||
void AudioDevice::retryNow() { m_retryAt = 0; m_retryDelay = 250; } |
||||
@ -0,0 +1,38 @@
|
||||
#pragma once |
||||
|
||||
#include "miniaudio.h" |
||||
#include <QElapsedTimer> |
||||
#include <atomic> |
||||
#include <functional> |
||||
|
||||
/* Один владелец в GUI-потоке. Callback публикует только прогресс и уведомления;
|
||||
* close() дожидается worker перед освобождением callback и его состояния. */ |
||||
class AudioDevice { |
||||
public: |
||||
using Callback = std::function<void(const void*, void*, unsigned int)>; |
||||
AudioDevice(); |
||||
~AudioDevice(); |
||||
AudioDevice(const AudioDevice&) = delete; |
||||
AudioDevice& operator=(const AudioDevice&) = delete; |
||||
bool init(ma_context* context, ma_device_config config, const char* role, uint64_t session, Callback callback); |
||||
bool start(); |
||||
void close(); |
||||
bool needsRecovery(); |
||||
bool retryReady() const; |
||||
void retryLater(); |
||||
void retryNow(); |
||||
bool initialized() const { return m_device != nullptr; } |
||||
ma_device* device() const { return m_device; } |
||||
private: |
||||
static void data(ma_device* device, void* out, const void* in, ma_uint32 frames); |
||||
static void notification(const ma_device_notification* event); |
||||
ma_device* m_device = nullptr; |
||||
Callback m_callback; |
||||
const char* m_role = "audio"; |
||||
QElapsedTimer m_clock; |
||||
std::atomic<uint64_t> m_frames{0}; |
||||
std::atomic<bool> m_suspended{false}; |
||||
uint64_t m_seenFrames = 0; |
||||
qint64 m_progressAt = 0, m_retryAt = 0; |
||||
int m_retryDelay = 250; |
||||
}; |
||||
@ -1,232 +1,164 @@
|
||||
#include "call_audio_engine.h" |
||||
#include "sound_manager.h" |
||||
#include "../transport/gui_bridge.h" |
||||
|
||||
#include "miniaudio.h" |
||||
|
||||
#include "../transport/audio_diagnostics.h" |
||||
extern "C" { |
||||
#include "call/call_audio.h" |
||||
#include "../../lib/debug_config.h" |
||||
#include "../../lib/u_async.h" |
||||
#include "debug_config.h" |
||||
} |
||||
|
||||
#include <QTimer> |
||||
#include <algorithm> |
||||
#include <cstring> |
||||
|
||||
static CallAudioEngine* g_callAudioEngine = nullptr; |
||||
|
||||
static void callAudioDataCallback(ma_device* dev, void* out, const void* in, ma_uint32 frames) { |
||||
CallAudioEngine* e = (CallAudioEngine*)dev->pUserData; |
||||
if (e) e->dataCallback(in, out, frames); |
||||
CallAudioEngine* CallAudioEngine::instance() { |
||||
static auto* engine = new CallAudioEngine(); |
||||
return engine; |
||||
} |
||||
|
||||
CallAudioEngine* CallAudioEngine::instance() { |
||||
if (!g_callAudioEngine) g_callAudioEngine = new CallAudioEngine(); |
||||
return g_callAudioEngine; |
||||
CallAudioEngine::CallAudioEngine() { |
||||
m_playbackId = SoundManager::instance()->playbackDevice(); |
||||
m_captureId = SoundManager::instance()->captureDevice(); |
||||
connect(SoundManager::instance(), &SoundManager::devicesChanged, this, [this]() { |
||||
setPlaybackDevice(SoundManager::instance()->playbackDevice()); |
||||
setCaptureDevice(SoundManager::instance()->captureDevice()); |
||||
}); |
||||
m_recoveryTimer.setInterval(200); |
||||
connect(&m_recoveryTimer, &QTimer::timeout, this, &CallAudioEngine::recoverDevices); |
||||
connect(SoundManager::instance(), &SoundManager::contextAboutToReset, this, &CallAudioEngine::pauseDevices); |
||||
connect(SoundManager::instance(), &SoundManager::contextRestored, this, [this]() { |
||||
m_capture.retryNow(); m_playback.retryNow(); |
||||
if (m_active) recoverDevices(); |
||||
}); |
||||
} |
||||
|
||||
int CallAudioEngine::start(quint64 callId) { |
||||
if (m_active) { |
||||
if (m_callId == callId) return 0; |
||||
stop(); |
||||
} |
||||
m_callId = callId; |
||||
|
||||
struct UTUN_INSTANCE* inst = gui_bridge_get_inst(); |
||||
if (!inst) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: no instance, cannot start"); |
||||
return -1; |
||||
} |
||||
if (call_audio_init(inst) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "CallAudio: call_audio_init failed"); |
||||
return -1; |
||||
} |
||||
if (call_audio_start(callId) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "CallAudio: call_audio_start failed call=%016llx", (unsigned long long)callId); |
||||
if (m_active && m_callId == callId && !m_ending) return 0; |
||||
stop(); |
||||
if (!gui_bridge_get_inst() || call_audio_start(callId) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "CallAudio: cannot start call=%016llx", (unsigned long long)callId); |
||||
return -1; |
||||
} |
||||
|
||||
++m_generation; |
||||
m_callId = callId; |
||||
m_captureBuf.assign(CALL_AUDIO_FRAME_SAMPLES, 0); |
||||
m_capturePos = 0; |
||||
m_muted = false; |
||||
|
||||
if (initDevice() != 0) { |
||||
call_audio_release(callId); |
||||
return -1; |
||||
} |
||||
|
||||
m_ending = false; |
||||
m_active = true; |
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: started call=%016llx", (unsigned long long)callId); |
||||
m_capture.retryNow(); m_playback.retryNow(); |
||||
recoverDevices(); |
||||
m_recoveryTimer.start(); |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "CallAudio: session started call=%016llx", (unsigned long long)callId); |
||||
return 0; |
||||
} |
||||
|
||||
int CallAudioEngine::initDevice() { |
||||
ma_context* ctx = SoundManager::instance()->context(); |
||||
if (!ctx) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: no audio context"); |
||||
return -1; |
||||
bool CallAudioEngine::openDevice(bool playback) { |
||||
AudioDevice& device = playback ? m_playback : m_capture; |
||||
ma_context* context = SoundManager::instance()->context(); |
||||
if (!context) return false; |
||||
ma_device_config config = ma_device_config_init(playback ? ma_device_type_playback : ma_device_type_capture); |
||||
config.capture.format = config.playback.format = ma_format_s16; |
||||
config.capture.channels = config.playback.channels = 1; |
||||
config.sampleRate = CALL_AUDIO_SAMPLE_RATE; |
||||
ma_device_id id; |
||||
bool selected = SoundManager::deviceId(playback ? m_playbackId : m_captureId, &id); |
||||
if (selected) { |
||||
if (playback) config.playback.pDeviceID = &id; |
||||
else config.capture.pDeviceID = &id; |
||||
} |
||||
auto callback = [this](const void* in, void* out, unsigned int frames) { dataCallback(in, out, frames); }; |
||||
const char* role = playback ? "call-playback" : "call-capture"; |
||||
auto attempt = [&]() { |
||||
if (!playback) m_capturePos = 0; |
||||
bool ready = device.init(context, config, role, m_callId.load(), callback) && device.start(); |
||||
if (!ready) call_audio_reset_io(m_callId.load(), playback); |
||||
return ready; |
||||
}; |
||||
if (attempt()) return true; |
||||
if (!selected) return false; |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "CallAudio: %s selected device unavailable, trying default", role); |
||||
config.capture.pDeviceID = config.playback.pDeviceID = nullptr; |
||||
return attempt(); |
||||
} |
||||
|
||||
m_device = new ma_device; |
||||
std::memset(m_device, 0, sizeof(*m_device)); |
||||
ma_device_config config = ma_device_config_init(ma_device_type_duplex); |
||||
config.capture.format = ma_format_s16; |
||||
config.capture.channels = 1; |
||||
config.playback.format = ma_format_s16; |
||||
config.playback.channels = 1; |
||||
config.sampleRate = CALL_AUDIO_SAMPLE_RATE; |
||||
config.periodSizeInFrames = 480; |
||||
config.dataCallback = callAudioDataCallback; |
||||
config.pUserData = this; |
||||
|
||||
if (m_playbackDeviceIndex >= 0) { |
||||
ma_device_info* pPlaybackDevices = nullptr; |
||||
ma_uint32 playbackCount = 0; |
||||
if (ma_context_get_devices(ctx, &pPlaybackDevices, &playbackCount, nullptr, nullptr) == MA_SUCCESS && |
||||
(ma_uint32)m_playbackDeviceIndex < playbackCount) { |
||||
config.playback.pDeviceID = &pPlaybackDevices[m_playbackDeviceIndex].id; |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "CallAudio: using playback device[%d]: %s", |
||||
m_playbackDeviceIndex, pPlaybackDevices[m_playbackDeviceIndex].name); |
||||
} else { |
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: playback device[%d] not found, using default", |
||||
m_playbackDeviceIndex); |
||||
} |
||||
void CallAudioEngine::recoverDevices() { |
||||
if (!m_active) return; |
||||
if (m_playback.needsRecovery()) { |
||||
m_playback.close(); |
||||
call_audio_reset_io(m_callId.load(), 1); |
||||
m_playback.retryLater(); |
||||
} |
||||
|
||||
if (ma_device_init(ctx, &config, m_device) != MA_SUCCESS) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: device init failed, trying default"); |
||||
config.playback.pDeviceID = nullptr; |
||||
if (ma_device_init(ctx, &config, m_device) != MA_SUCCESS) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "CallAudio: default device init also failed"); |
||||
delete m_device; m_device = nullptr; |
||||
return -1; |
||||
} |
||||
if (!m_ending && m_capture.needsRecovery()) { |
||||
m_capture.close(); |
||||
m_capturePos = 0; |
||||
call_audio_reset_io(m_callId.load(), 0); |
||||
m_capture.retryLater(); |
||||
} |
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: device backend=%s rate=%u capture='%s' rate=%u period=%u playback='%s' rate=%u period=%u", |
||||
ma_get_backend_name(ctx->backend), m_device->sampleRate, |
||||
m_device->capture.name, m_device->capture.internalSampleRate, m_device->capture.internalPeriodSizeInFrames, |
||||
m_device->playback.name, m_device->playback.internalSampleRate, m_device->playback.internalPeriodSizeInFrames); |
||||
if (m_playback.retryReady()) openDevice(true); |
||||
if (!m_ending && m_capture.retryReady()) openDevice(false); |
||||
} |
||||
|
||||
void CallAudioEngine::pauseDevices() { |
||||
m_capture.close(); m_playback.close(); |
||||
m_capturePos = 0; |
||||
m_dbg = {}; |
||||
if (ma_device_start(m_device) != MA_SUCCESS) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: device start failed"); |
||||
ma_device_uninit(m_device); |
||||
delete m_device; m_device = nullptr; |
||||
return -1; |
||||
} |
||||
m_deviceStarted = true; |
||||
return 0; |
||||
if (m_active) call_audio_reset_io(m_callId.load(), 1); |
||||
} |
||||
|
||||
void CallAudioEngine::setMuted(bool muted) { |
||||
if (m_muted.load() == muted) return; |
||||
m_muted = muted; |
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: mute %s", muted ? "on" : "off"); |
||||
if (m_muted.exchange(muted) == muted) return; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "CallAudio: mute=%d", muted); |
||||
} |
||||
|
||||
void CallAudioEngine::setPlaybackDeviceIndex(int index) { |
||||
if (index == m_playbackDeviceIndex) return; |
||||
int prev = m_playbackDeviceIndex; |
||||
m_playbackDeviceIndex = index; |
||||
if (m_active) { |
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: switch playback device %d -> %d", prev, index); |
||||
teardownDevice(); |
||||
if (initDevice() != 0) |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "CallAudio: switch playback device failed (kept index=%d)", index); |
||||
} |
||||
void CallAudioEngine::setPlaybackDevice(const QString& id) { |
||||
if (id == m_playbackId) return; |
||||
m_playbackId = id; |
||||
m_playback.close(); m_playback.retryNow(); |
||||
if (m_active) { call_audio_reset_io(m_callId.load(), 1); recoverDevices(); } |
||||
} |
||||
|
||||
void CallAudioEngine::setCaptureDevice(const QString& id) { |
||||
if (id == m_captureId) return; |
||||
m_captureId = id; |
||||
m_capture.close(); m_capture.retryNow(); |
||||
m_capturePos = 0; |
||||
if (m_active) { call_audio_reset_io(m_callId.load(), 0); recoverDevices(); } |
||||
} |
||||
|
||||
void CallAudioEngine::beginEnd(quint64 callId) { |
||||
if (!m_active || m_callId != callId) return; |
||||
call_audio_begin_end(callId); /* старт тона завершения */ |
||||
QTimer::singleShot(500, this, [this, callId]() { |
||||
if (m_callId == callId) stop(); |
||||
}); |
||||
if (!m_active || m_callId != callId || m_ending) return; |
||||
m_ending = true; |
||||
m_capture.close(); |
||||
call_audio_begin_end(callId); |
||||
uint64_t generation = m_generation; |
||||
QTimer::singleShot(500, this, [this, generation]() { if (m_generation == generation) stop(); }); |
||||
} |
||||
|
||||
void CallAudioEngine::stop() { |
||||
if (!m_active && !m_device && !m_deviceStarted) return; |
||||
++m_generation; |
||||
m_recoveryTimer.stop(); |
||||
m_active = false; |
||||
pauseDevices(); |
||||
uint64_t id = m_callId.exchange(0); |
||||
if (id) call_audio_release(id); |
||||
m_muted = false; |
||||
quint64 cid = m_callId.load(); |
||||
|
||||
teardownDevice(); |
||||
call_audio_release(cid); |
||||
|
||||
m_callId = 0; |
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: stopped"); |
||||
} |
||||
|
||||
void CallAudioEngine::teardownDevice() { |
||||
if (m_deviceStarted) { |
||||
ma_device_stop(m_device); |
||||
ma_device_uninit(m_device); |
||||
m_deviceStarted = false; |
||||
} |
||||
if (m_device) { delete m_device; m_device = nullptr; } |
||||
m_ending = false; |
||||
if (id) DEBUG_INFO(DEBUG_CATEGORY_CALL, "CallAudio: session stopped call=%016llx", (unsigned long long)id); |
||||
} |
||||
|
||||
void CallAudioEngine::dataCallback(const void* in, void* out, unsigned int frames) { |
||||
const int16_t* in16 = (const int16_t*)in; |
||||
int16_t* out16 = (int16_t*)out; |
||||
uint64_t startTb = get_time_tb(); |
||||
uint64_t idleTb = m_dbg.lastEndTb ? startTb - m_dbg.lastEndTb : 0; |
||||
if (!m_dbg.lastReportTb) m_dbg.lastReportTb = startTb; |
||||
|
||||
/* диагностика (DEBUG_CATEGORY_DEBUG): фактические параметры miniaudio-коллбэка */ |
||||
if (m_dbg.frames != frames) { |
||||
m_dbg.frames = frames; |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "CallAudio: callback frames=%u in=%s out=%s", |
||||
frames, in16 ? "yes" : "no", out16 ? "yes" : "no"); |
||||
} |
||||
|
||||
/* захват: копим 960 samples → encode+send (сервисный слой). mute → тишина. */ |
||||
uint64_t captureStartTb = get_time_tb(); |
||||
if (in16) { |
||||
if (in) { |
||||
const auto* pcm = static_cast<const int16_t*>(in); |
||||
bool muted = m_muted.load(); |
||||
for (unsigned int i = 0; i < frames; i++) { |
||||
m_captureBuf[m_capturePos++] = muted ? 0 : in16[i]; |
||||
if (m_capturePos >= (int)m_captureBuf.size()) { |
||||
call_audio_feed_pcm(m_callId.load(), m_captureBuf.data(), (int)m_captureBuf.size()); |
||||
for (unsigned int i = 0; i < frames; ++i) { |
||||
m_captureBuf[m_capturePos++] = muted ? 0 : pcm[i]; |
||||
if (m_capturePos == CALL_AUDIO_FRAME_SAMPLES) { |
||||
int result = call_audio_feed_pcm(m_callId.load(), m_captureBuf.data(), CALL_AUDIO_FRAME_SAMPLES); |
||||
if (result != 0) audio_diag_event(m_capture.device(), AUDIO_DIAG_ERROR, AUDIO_DIAG_FEED, 1, result); |
||||
m_capturePos = 0; |
||||
} |
||||
} |
||||
} |
||||
uint64_t captureEndTb = get_time_tb(); |
||||
|
||||
/* воспроизведение: финальный PCM из сервисного слоя (jitter+stretch+тоны) */ |
||||
int n = call_audio_pull_pcm(m_callId.load(), out16, (int)frames); |
||||
if (n < (int)frames) |
||||
std::memset(out16 + n, 0, (size_t)(frames - n) * sizeof(int16_t)); |
||||
uint64_t endTb = get_time_tb(); |
||||
uint64_t captureTb = captureEndTb - captureStartTb; |
||||
uint64_t playbackTb = endTb - captureEndTb; |
||||
m_dbg.callbacks++; |
||||
m_dbg.captureFrames += in16 ? frames : 0; |
||||
m_dbg.playbackFrames += out16 ? frames : 0; |
||||
m_dbg.maxIdleTb = std::max(m_dbg.maxIdleTb, idleTb); |
||||
m_dbg.maxCaptureTb = std::max(m_dbg.maxCaptureTb, captureTb); |
||||
m_dbg.maxPlaybackTb = std::max(m_dbg.maxPlaybackTb, playbackTb); |
||||
if (idleTb > 500 || endTb - startTb > 200) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: stall call=%016llx idle=%.1fms work=%.1fms capture=%.1fms playback=%.1fms frames=%u", |
||||
(unsigned long long)m_callId.load(), idleTb / 10.0, (endTb - startTb) / 10.0, |
||||
captureTb / 10.0, playbackTb / 10.0, frames); |
||||
} |
||||
uint64_t elapsedTb = endTb - m_dbg.lastReportTb; |
||||
if (elapsedTb >= 10000) { |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, |
||||
"CallAudio: interval=%.1fms cb=%.1f/s capture=%.0f/s playback=%.0f/s max_idle=%.1fms max_capture=%.1fms max_playback=%.1fms", |
||||
elapsedTb / 10.0, m_dbg.callbacks * 10000.0 / elapsedTb, |
||||
m_dbg.captureFrames * 10000.0 / elapsedTb, m_dbg.playbackFrames * 10000.0 / elapsedTb, |
||||
m_dbg.maxIdleTb / 10.0, m_dbg.maxCaptureTb / 10.0, m_dbg.maxPlaybackTb / 10.0); |
||||
m_dbg.callbacks = 0; |
||||
m_dbg.captureFrames = 0; |
||||
m_dbg.playbackFrames = 0; |
||||
m_dbg.maxIdleTb = 0; |
||||
m_dbg.maxCaptureTb = 0; |
||||
m_dbg.maxPlaybackTb = 0; |
||||
m_dbg.lastReportTb = endTb; |
||||
if (out) { |
||||
auto* pcm = static_cast<int16_t*>(out); |
||||
int n = call_audio_pull_pcm(m_callId.load(), pcm, (int)frames); |
||||
if (n < (int)frames) std::memset(pcm + n, 0, (frames - n) * sizeof(*pcm)); |
||||
} |
||||
m_dbg.lastEndTb = get_time_tb(); |
||||
} |
||||
|
||||
@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3 |
||||
"""Изолированный PipeWire/PulseAudio с виртуальным sink; системный сервер не затрагивается.""" |
||||
import os |
||||
from pathlib import Path |
||||
import shutil |
||||
import signal |
||||
import subprocess |
||||
import sys |
||||
import tempfile |
||||
import time |
||||
|
||||
|
||||
def wait_for(predicate, seconds=10): |
||||
deadline = time.monotonic() + seconds |
||||
while time.monotonic() < deadline: |
||||
if predicate(): |
||||
return |
||||
time.sleep(0.02) |
||||
raise RuntimeError("private audio server/test did not reach expected state") |
||||
|
||||
|
||||
def main(): |
||||
for executable in ("pipewire", "pipewire-pulse", "wireplumber", "dbus-run-session", "pactl"): |
||||
if not shutil.which(executable): |
||||
raise RuntimeError(f"required executable missing: {executable}") |
||||
binary = Path(sys.argv[1] if len(sys.argv) > 1 else "tools/chatgui/build/test_audio_recovery").resolve() |
||||
with tempfile.TemporaryDirectory(prefix="utun-pulse-") as root: |
||||
directory = Path(root) |
||||
config = directory / "config/wireplumber/wireplumber.conf.d" |
||||
config.mkdir(parents=True) |
||||
(config / "test.conf").write_text('''wireplumber.profiles = { |
||||
main = { |
||||
monitor.alsa = disabled |
||||
monitor.alsa-midi = disabled |
||||
monitor.bluez = disabled |
||||
monitor.bluez-midi = disabled |
||||
monitor.v4l2 = disabled |
||||
monitor.libcamera = disabled |
||||
} |
||||
}''') |
||||
env = os.environ.copy() |
||||
env.update(XDG_RUNTIME_DIR=root, PIPEWIRE_RUNTIME_DIR=root, XDG_CONFIG_HOME=str(directory / "config"), |
||||
XDG_STATE_HOME=str(directory / "state"), PIPEWIRE_REMOTE="pipewire-0", PULSE_SERVER=f"unix:{root}/pulse/native") |
||||
processes, logs = [], [] |
||||
|
||||
def launch(command, label): |
||||
log = (directory / f"{label}.log").open("w+") |
||||
logs.append(log) |
||||
process = subprocess.Popen(command, env=env, stdout=log, stderr=subprocess.STDOUT, start_new_session=True) |
||||
processes.append(process) |
||||
return process |
||||
|
||||
def pulse_ready(): |
||||
return subprocess.run(["pactl", "info"], env=env, stdout=subprocess.DEVNULL, |
||||
stderr=subprocess.DEVNULL, timeout=2).returncode == 0 |
||||
|
||||
try: |
||||
launch(["pipewire"], "pipewire") |
||||
wait_for(lambda: (directory / "pipewire-0").exists()) |
||||
launch(["dbus-run-session", "--", "wireplumber"], "wireplumber") |
||||
pulse = launch(["pipewire-pulse"], "pulse-before") |
||||
wait_for(pulse_ready) |
||||
# module-always-sink создаёт виртуальный dummy sink и его monitor source. |
||||
test = launch([str(binary), "--pulse-integration", root], "client") |
||||
wait_for(lambda: (directory / "ready").exists() or test.poll() is not None) |
||||
if test.poll() is not None: |
||||
raise RuntimeError("client failed before server restart") |
||||
pulse.terminate(); pulse.wait(timeout=5) |
||||
wait_for(lambda: (directory / "closed").exists() or test.poll() is not None) |
||||
launch(["pipewire-pulse"], "pulse-after") |
||||
wait_for(pulse_ready) |
||||
if test.wait(timeout=15) != 0: |
||||
raise RuntimeError("client recovery failed") |
||||
logs[3].flush(); logs[3].seek(0) |
||||
print(logs[3].read()) |
||||
print("private PulseAudio server restart: PASS") |
||||
except Exception: |
||||
for log in logs: |
||||
log.flush(); log.seek(0) |
||||
print(f"--- {Path(log.name).name} ---\n{log.read()}", file=sys.stderr) |
||||
raise |
||||
finally: |
||||
for process in reversed(processes): |
||||
if process.poll() is None: |
||||
os.killpg(process.pid, signal.SIGTERM) |
||||
try: |
||||
process.wait(timeout=5) |
||||
except subprocess.TimeoutExpired: |
||||
os.killpg(process.pid, signal.SIGKILL); process.wait() |
||||
for log in logs: |
||||
log.close() |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
main() |
||||
@ -0,0 +1,42 @@
|
||||
/* Проверяем настоящую очередь: конкуренция, переполнение, целостность и повторный запуск. */ |
||||
#include "../transport/audio_diagnostics.cpp" |
||||
#include <cassert> |
||||
#include <cstdio> |
||||
#include <vector> |
||||
|
||||
int main() { |
||||
debug_config_init(); debug_enable_console(0); |
||||
constexpr int producers = 4, perProducer = 20000; |
||||
std::vector<bool> seen(producers * perProducer); |
||||
std::atomic<int> done{0}; |
||||
for (uint64_t i = 0; i < kCapacity; ++i) queue[i].sequence.store(i); |
||||
running = true; |
||||
// Сначала полное кольцо: producer обязан вернуть управление, потеря учтена.
|
||||
for (uint64_t i = 0; i < kCapacity + 1; ++i) audio_diag_event(nullptr, AUDIO_DIAG_HOLE, AUDIO_DIAG_READ, i, 7); |
||||
assert(lost.load() == 1); |
||||
Event e; |
||||
for (uint64_t i = 0; i < kCapacity; ++i) { |
||||
assert(take(e)); assert(e.value == int64_t(i) && e.detail == 7); |
||||
} |
||||
assert(!take(e)); lost = 0; |
||||
std::vector<std::thread> workers; |
||||
for (int p = 0; p < producers; ++p) workers.emplace_back([p, &done]() { |
||||
for (int i = 0; i < perProducer; ++i) |
||||
audio_diag_event(nullptr, AUDIO_DIAG_HOLE, AUDIO_DIAG_READ, p * perProducer + i, p); |
||||
++done; |
||||
}); |
||||
uint64_t received = 0; |
||||
while (done.load() != producers || readPosition != writePosition.load()) { |
||||
if (!take(e)) { std::this_thread::yield(); continue; } |
||||
assert(e.value >= 0 && e.value < producers * perProducer); |
||||
assert(e.kind == AUDIO_DIAG_HOLE && e.stage == AUDIO_DIAG_READ && e.value / perProducer == e.detail); |
||||
assert(e.wall && e.cpu && e.tid && !seen[e.value]); |
||||
seen[e.value] = true; ++received; |
||||
} |
||||
for (auto& worker : workers) worker.join(); |
||||
assert(received + lost.load() == producers * perProducer); |
||||
running = false; |
||||
// Lifecycle не оставляет joinable-поток и очищает очередь между сеансами.
|
||||
for (int i = 0; i < 2; ++i) { audio_diag_start(); audio_diag_event(nullptr, AUDIO_DIAG_OPEN, -1, 1, 48000); audio_diag_stop(); } |
||||
puts("test_audio_diagnostics: bounded MPSC, concurrent integrity and restart PASS"); |
||||
} |
||||
@ -0,0 +1,222 @@
|
||||
/* Настоящие null devices и Qt timers; linker wrappers инъецируют только сбои lifecycle. */ |
||||
#include "../src/audio_device.h" |
||||
#include "../src/call_audio_engine.h" |
||||
#include "../src/radio_audio_engine.h" |
||||
#include "../src/audiorecorder.h" |
||||
#include "../src/sound_manager.h" |
||||
#include <QCoreApplication> |
||||
#include <QThread> |
||||
#include <QFile> |
||||
#include <cstring> |
||||
#include <algorithm> |
||||
#include <map> |
||||
#include <vector> |
||||
extern "C" { |
||||
#include "utun_instance.h" |
||||
#include "call/call_audio.h" |
||||
#include "radio/radio_audio.h" |
||||
#include "media_async/attachment_send.h" |
||||
#include "u_async.h" |
||||
#include "debug_config.h" |
||||
} |
||||
|
||||
#define REQUIRE(condition) do { if (!(condition)) qFatal("audio recovery line %d: %s", __LINE__, #condition); } while (0) |
||||
static UTUN_INSTANCE instance{}; |
||||
static std::map<ma_context*, int> live; |
||||
static std::vector<ma_device*> devices; |
||||
static int starts, captureStarts, playbackStarts, contextStarts, failContexts; |
||||
static bool failStart, failContext, failDevice; |
||||
static bool pulseIntegration; |
||||
|
||||
extern "C" UTUN_INSTANCE* gui_bridge_get_inst() { return &instance; } |
||||
extern "C" ma_result __real_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context*); |
||||
extern "C" void __real_ma_context_uninit(ma_context*); |
||||
extern "C" ma_result __real_ma_device_init(ma_context*, const ma_device_config*, ma_device*); |
||||
extern "C" ma_result __real_ma_device_start(ma_device*); |
||||
extern "C" void __real_ma_device_uninit(ma_device*); |
||||
extern "C" int __real_audio_context_failed(ma_context*); |
||||
|
||||
extern "C" ma_result __wrap_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context* context) { |
||||
++contextStarts; |
||||
if (failContexts > 0) { --failContexts; return MA_ERROR; } |
||||
ma_backend backend = pulseIntegration ? ma_backend_pulseaudio : ma_backend_null; |
||||
return __real_ma_context_init(&backend, 1, nullptr, context); |
||||
} |
||||
extern "C" void __wrap_ma_context_uninit(ma_context* context) { |
||||
REQUIRE(live[context] == 0); |
||||
__real_ma_context_uninit(context); |
||||
} |
||||
extern "C" ma_result __wrap_ma_device_init(ma_context* context, const ma_device_config* config, ma_device* device) { |
||||
if (failDevice) { failDevice = false; return MA_ERROR; } |
||||
ma_result result = __real_ma_device_init(context, config, device); |
||||
if (result == MA_SUCCESS) { ++live[context]; devices.push_back(device); } |
||||
return result; |
||||
} |
||||
extern "C" ma_result __wrap_ma_device_start(ma_device* device) { |
||||
++starts; |
||||
if (device->type == ma_device_type_capture) { |
||||
++captureStarts; |
||||
// Некоторые backends вызывают data callback ещё до возврата start.
|
||||
std::vector<int16_t> input(1200 * device->capture.channels, 1000); |
||||
if (!pulseIntegration) device->onData(device, nullptr, input.data(), 1200); |
||||
} else ++playbackStarts; |
||||
if (failStart) { failStart = false; return MA_ERROR; } |
||||
return __real_ma_device_start(device); |
||||
} |
||||
extern "C" void __wrap_ma_device_uninit(ma_device* device) { |
||||
auto context = device->pContext; |
||||
__real_ma_device_uninit(device); |
||||
REQUIRE(live[context] > 0); |
||||
--live[context]; |
||||
devices.erase(std::find(devices.begin(), devices.end(), device)); |
||||
} |
||||
extern "C" int __wrap_audio_context_failed(ma_context* context) { |
||||
if (failContext) { failContext = false; return 1; } |
||||
return __real_audio_context_failed(context); |
||||
} |
||||
|
||||
static void pump(int milliseconds) { |
||||
QElapsedTimer timer; timer.start(); |
||||
while (timer.elapsed() < milliseconds) { QCoreApplication::processEvents(); QThread::msleep(1); } |
||||
} |
||||
|
||||
static void pulse_recovery(SoundManager* manager, const QString& control) { |
||||
REQUIRE(manager->context()->backend == ma_backend_pulseaudio); |
||||
AudioRecorder recorder; recorder.init(); recorder.startRecording(); |
||||
std::vector<int16_t> pcm(48000 * 20, 1000); |
||||
manager->playRawPcm(pcm.data(), (int)pcm.size(), 48000, 1); |
||||
for (int i = 0; i < 100 && recorder.durationMs() < 300; ++i) pump(50); |
||||
REQUIRE(recorder.durationMs() >= 300 && manager->currentPcmCursor() > 0); |
||||
int duration = recorder.durationMs(); |
||||
auto cursor = manager->currentPcmCursor(); |
||||
QFile ready(control + "/ready"); REQUIRE(ready.open(QIODevice::WriteOnly)); ready.close(); |
||||
for (int i = 0; i < 100 && manager->context(); ++i) pump(50); |
||||
REQUIRE(!manager->context()); |
||||
QFile closed(control + "/closed"); REQUIRE(closed.open(QIODevice::WriteOnly)); closed.close(); |
||||
for (int i = 0; i < 150 && (!manager->context() || recorder.durationMs() <= duration + 200); ++i) pump(50); |
||||
REQUIRE(manager->context() && recorder.durationMs() > duration + 200 && manager->currentPcmCursor() > cursor); |
||||
qInfo("PulseAudio progress: capture=%d -> %dms playback=%llu -> %llu frames", duration, recorder.durationMs(), |
||||
(unsigned long long)cursor, manager->currentPcmCursor()); |
||||
recorder.stopRecording(); manager->shutdown(); |
||||
REQUIRE(devices.empty()); |
||||
qInfo("real PulseAudio: capture/playback progress restored after server restart PASS"); |
||||
} |
||||
|
||||
int main(int argc, char** argv) { |
||||
QCoreApplication app(argc, argv); |
||||
pulseIntegration = argc == 3 && std::strcmp(argv[1], "--pulse-integration") == 0; |
||||
debug_config_init(); debug_set_level(pulseIntegration ? DEBUG_LEVEL_INFO : DEBUG_LEVEL_WARN); |
||||
auto* manager = SoundManager::instance(); |
||||
REQUIRE(manager->init() && manager->context()); |
||||
if (pulseIntegration) { pulse_recovery(manager, QString::fromLocal8Bit(argv[2])); return 0; } |
||||
ma_device_id id{}; |
||||
auto outputs = manager->enumeratePlaybackDevices(); |
||||
REQUIRE(!outputs.empty() && SoundManager::deviceId(outputs.front().id, &id)); |
||||
REQUIRE(!SoundManager::deviceId(outputs.front().id + '!', &id)); |
||||
{ |
||||
AudioDevice device; |
||||
auto config = ma_device_config_init(ma_device_type_capture); |
||||
config.capture.format = ma_format_s16; config.capture.channels = 1; config.sampleRate = 48000; |
||||
REQUIRE(device.init(manager->context(), config, "test-partial-start", 0, [](const void*, void*, unsigned int) {})); |
||||
int count = live[manager->context()]; |
||||
failStart = true; |
||||
REQUIRE(!device.start() && !device.initialized()); |
||||
REQUIRE(live[manager->context()] == count - 1); |
||||
failDevice = true; |
||||
REQUIRE(!device.init(manager->context(), config, "test-init-failure", 0, [](const void*, void*, unsigned int) {})); |
||||
REQUIRE(!device.initialized() && live[manager->context()] == count - 1); |
||||
} |
||||
{ |
||||
AudioRecorder first, second; |
||||
first.init(); second.init(); |
||||
auto inputs = manager->enumerateCaptureDevices(); |
||||
REQUIRE(!inputs.empty()); |
||||
first.setCaptureDevice(inputs.front().id); |
||||
int previous = captureStarts; |
||||
failStart = true; |
||||
first.startRecording(); second.startRecording(); |
||||
REQUIRE(captureStarts == previous + 3); // Выбранный input не стартовал: default запущен после полного uninit.
|
||||
pump(60); |
||||
first.stopRecording(); second.stopRecording(); |
||||
attachment_send_req a{}, b{}; |
||||
REQUIRE(first.takeRecording(&a) && second.takeRecording(&b)); |
||||
REQUIRE(a.pcm_count >= 1200 && b.pcm_count >= 1200); |
||||
REQUIRE(a.pcm[0] == 1000 && b.pcm[0] == 1000); // pUserData маршрутизирует каждый callback своему recorder.
|
||||
a.pcm_release(a.pcm_owner); b.pcm_release(b.pcm_owner); |
||||
} |
||||
instance.ua = uasync_create(); REQUIRE(instance.ua); |
||||
REQUIRE(call_audio_init(&instance) == 0 && radio_audio_init(&instance) == 0); |
||||
auto* call = CallAudioEngine::instance(); |
||||
REQUIRE(call->start(42) == 0); |
||||
pump(60); |
||||
ma_device* playback = nullptr; |
||||
for (auto* device : devices) |
||||
if (device->type == ma_device_type_playback && device->playback.format == ma_format_s16 && device->playback.channels == 1) |
||||
playback = device; |
||||
REQUIRE(playback); |
||||
int previousCapture = captureStarts, previousPlayback = playbackStarts; |
||||
REQUIRE(ma_device_stop(playback) == MA_SUCCESS); |
||||
pump(800); |
||||
REQUIRE(captureStarts == previousCapture && playbackStarts > previousPlayback); |
||||
REQUIRE(call->callId() == 42 && call->isActive()); |
||||
auto* radio = RadioAudioEngine::instance(); |
||||
REQUIRE(radio->start(7) == 0); |
||||
AudioRecorder recorder; recorder.init(); recorder.startRecording(); |
||||
std::vector<int16_t> notification(48000 * 10, 1000); |
||||
manager->playRawPcm(notification.data(), (int)notification.size(), 48000, 1); |
||||
pump(60); |
||||
auto cursor = manager->currentPcmCursor(); |
||||
REQUIRE(cursor > 0); |
||||
int previousContext = contextStarts; |
||||
failContext = true; failContexts = 2; |
||||
pump(1800); |
||||
REQUIRE(contextStarts >= previousContext + 3 && manager->context()); |
||||
REQUIRE(call->callId() == 42 && radio->groupId() == 7 && recorder.isRecording()); |
||||
REQUIRE(manager->currentPcmCursor() > cursor && manager->isCurrentPcmPlaying()); |
||||
call->stop(); radio->stop(); recorder.stopRecording(); |
||||
int previousStarts = starts; |
||||
pump(600); |
||||
REQUIRE(starts == previousStarts); // Отложенное recovery не воскрешает остановленную сессию.
|
||||
REQUIRE(call->start(43) == 0); |
||||
call->beginEnd(43); |
||||
REQUIRE(call->start(43) == 0); |
||||
pump(600); |
||||
REQUIRE(call->isActive() && call->callId() == 43); // Старый end timer не закрывает новое поколение того же call ID.
|
||||
call->stop(); |
||||
failContext = true; failContexts = 1; |
||||
recorder.startRecording(); |
||||
pump(220); |
||||
REQUIRE(!manager->context()); |
||||
recorder.stopRecording(); |
||||
previousCapture = captureStarts; |
||||
pump(1000); |
||||
REQUIRE(manager->context() && captureStarts == previousCapture); |
||||
|
||||
// Предел TX действует до помещения задачи в uasync; старое поколение не отправляется после stop/start.
|
||||
uasync_poll(instance.ua, 0); |
||||
REQUIRE(call_audio_start(99) == 0); |
||||
int16_t silence[960]{}; |
||||
for (int i = 0; i < 100; ++i) call_audio_feed_pcm(99, silence, 960); |
||||
int pending = 0; |
||||
for (auto* task = instance.ua->posted_tasks_head; task; task = task->next) ++pending; |
||||
REQUIRE(pending == 8); |
||||
call_audio_stop(); |
||||
REQUIRE(call_audio_start(100) == 0); |
||||
for (int i = 0; i < 100; ++i) call_audio_feed_pcm(100, silence, 960); |
||||
pending = 0; |
||||
for (auto* task = instance.ua->posted_tasks_head; task; task = task->next) ++pending; |
||||
REQUIRE(pending == 8); // Новый звонок также учитывает ещё не освобождённые задачи старого.
|
||||
call_audio_stop(); |
||||
uasync_poll(instance.ua, 0); |
||||
REQUIRE(!instance.ua->posted_tasks_head); |
||||
REQUIRE(radio_audio_start(8, 1) == 0); |
||||
radio_audio_talk_begin(8); radio_audio_talk_end(8); radio_audio_stop(); |
||||
REQUIRE(instance.ua->posted_tasks_head); |
||||
// Завершение ядра освобождает ещё не выполненные PTT-задачи вместе с аргументами.
|
||||
call_audio_destroy(&instance); radio_audio_destroy(&instance); |
||||
uasync_destroy(instance.ua, 0); instance.ua = nullptr; |
||||
manager->shutdown(); |
||||
REQUIRE(devices.empty()); |
||||
qInfo("audio recovery: partial start, independent streams, two recorders, context restart, cancellation, bounded TX PASS"); |
||||
return 0; |
||||
} |
||||
@ -0,0 +1,163 @@
|
||||
/* Реальные ветки miniaudio на null backend: аппаратный звук и сеть не нужны. */ |
||||
#include "../transport/audio_diagnostics.h" |
||||
#include "../../../lib/speex_aec.h" |
||||
#include <assert.h> |
||||
#include <stdint.h> |
||||
#include <stdio.h> |
||||
#include <string.h> |
||||
|
||||
static uint64_t observed_unfilled, observed_no_progress, observed_holes; |
||||
static uint32_t callback_count; |
||||
static void test_event(const void* device, int kind, int stage, int64_t value, int64_t detail) { |
||||
if (kind == AUDIO_DIAG_UNFILLED) observed_unfilled += (uint64_t)value; |
||||
if (kind == AUDIO_DIAG_NO_PROGRESS) observed_no_progress += (uint64_t)value; |
||||
if (kind == AUDIO_DIAG_HOLE) observed_holes += (uint64_t)value; |
||||
audio_diag_event(device, kind, stage, value, detail); |
||||
} |
||||
#define MA_DIAGNOSTIC_EVENT(device, kind, stage, value, detail) test_event(device, kind, stage, value, detail) |
||||
#define MA_DIAGNOSTIC_LOG(log, level, message) audio_diag_log(log, level, message) |
||||
#define MA_PULSE_WAIT_TIMEOUT_MS 20 |
||||
#define MINIAUDIO_IMPLEMENTATION |
||||
#include "../../../lib/miniaudio.h" |
||||
|
||||
static void callback(ma_device* device, void* out, const void* in, ma_uint32 count) { |
||||
(void)device; (void)in; |
||||
callback_count++; |
||||
if (out) for (ma_uint32 i = 0; i < count; i++) ((int16_t*)out)[i] = 12000; |
||||
} |
||||
|
||||
static void starvation(int duplex) { |
||||
ma_backend backend = ma_backend_null; |
||||
ma_context context; |
||||
ma_device device; |
||||
ma_device_config config = ma_device_config_init(duplex ? ma_device_type_duplex : ma_device_type_playback); |
||||
config.capture.format = config.playback.format = ma_format_s16; |
||||
config.capture.channels = config.playback.channels = 1; |
||||
config.sampleRate = 48000; config.periodSizeInFrames = 480; config.dataCallback = callback; |
||||
assert(ma_context_init(&backend, 1, NULL, &context) == MA_SUCCESS); |
||||
assert(ma_device_init(&context, &config, &device) == MA_SUCCESS); |
||||
if (duplex) assert(ma_duplex_rb_init(ma_format_s16, 1, 48000, 48000, 480, NULL, &device.duplexRB) == MA_SUCCESS); |
||||
/* Worker не запускаем: стенд синхронно вызывает backend-обработчик. */ |
||||
ma_device__set_state(&device, ma_device_state_started); |
||||
audio_diag_label(&device, duplex ? 1 : 2, duplex ? "test-duplex" : "test-playback"); |
||||
int16_t input[480] = {0}, output[480]; |
||||
observed_unfilled = 0; callback_count = 0; |
||||
uint64_t untouched = 0; |
||||
for (int tick = 0; tick < 20; tick++) { |
||||
if (duplex && tick < 3) assert(ma_device_handle_backend_data_callback(&device, NULL, input, 480) == MA_SUCCESS); |
||||
for (int i = 0; i < 480; i++) output[i] = 2345; |
||||
assert(ma_device_handle_backend_data_callback(&device, output, NULL, 480) == MA_SUCCESS); |
||||
for (int i = 0; i < 480; i++) untouched += output[i] == 2345; |
||||
} |
||||
assert(untouched == observed_unfilled); |
||||
assert(untouched == 0 && callback_count >= 20); |
||||
printf("starvation duplex=%d unfilled=%llu callback_count=%u PASS\n", duplex, |
||||
(unsigned long long)untouched, callback_count); |
||||
ma_device__set_state(&device, ma_device_state_stopped); |
||||
if (duplex) ma_duplex_rb_uninit(&device.duplexRB); |
||||
ma_device_uninit(&device); ma_context_uninit(&context); |
||||
} |
||||
|
||||
#ifdef MA_SUPPORT_PULSEAUDIO |
||||
static size_t zero_writable(const ma_pa_stream* stream) { |
||||
(void)stream; |
||||
/* Без внешнего stop callback обязан вернуть управление сам. */ |
||||
return 0; |
||||
} |
||||
static void no_progress(void) { |
||||
ma_context context; |
||||
ma_device device; |
||||
memset(&context, 0, sizeof(context)); memset(&device, 0, sizeof(device)); |
||||
device.pContext = &context; |
||||
device.playback.internalFormat = ma_format_s16; device.playback.internalChannels = 1; |
||||
context.pulse.pa_stream_writable_size = (ma_proc)zero_writable; |
||||
ma_device__set_state(&device, ma_device_state_started); |
||||
observed_no_progress = 0; |
||||
ma_device_on_write__pulse((ma_pa_stream*)&device, 960, &device); |
||||
assert(observed_no_progress == 1); |
||||
audio_diag_event(&device, AUDIO_DIAG_CLOSE, -1, 0, 0); |
||||
puts("PulseAudio zero-progress detection PASS"); |
||||
} |
||||
static unsigned int wait_cancelled, wait_unrefed; |
||||
static ma_pa_operation_state_t wait_state(const ma_pa_operation* operation) { (void)operation; return MA_PA_OPERATION_RUNNING; } |
||||
static void wait_cancel(ma_pa_operation* operation) { (void)operation; ++wait_cancelled; } |
||||
static void wait_unref(ma_pa_operation* operation) { (void)operation; ++wait_unrefed; } |
||||
static int wait_iterate(ma_pa_mainloop* loop, int block, int* result) { |
||||
(void)loop; (void)result; |
||||
assert(block == 0); |
||||
return 0; |
||||
} |
||||
static void bounded_wait(void) { |
||||
ma_context context; |
||||
ma_timer timer; |
||||
memset(&context, 0, sizeof(context)); |
||||
context.pulse.pa_operation_get_state = (ma_proc)wait_state; |
||||
context.pulse.pa_operation_cancel = (ma_proc)wait_cancel; |
||||
context.pulse.pa_operation_unref = (ma_proc)wait_unref; |
||||
context.pulse.pa_mainloop_iterate = (ma_proc)wait_iterate; |
||||
wait_cancelled = wait_unrefed = 0; |
||||
ma_timer_init(&timer); |
||||
assert(ma_wait_for_operation_and_unref__pulse(&context, NULL, (ma_pa_operation*)&context) == MA_TIMEOUT); |
||||
assert(wait_cancelled == 1 && wait_unrefed == 1); |
||||
assert(ma_timer_get_time_in_seconds(&timer) < 0.5); |
||||
puts("PulseAudio deadline and callback cancellation PASS"); |
||||
} |
||||
|
||||
static unsigned int hole_frames, hole_drops; |
||||
static int hole_peek(ma_pa_stream* stream, const void** data, size_t* bytes) { |
||||
(void)stream; *data = NULL; *bytes = 9600; return 0; |
||||
} |
||||
static int hole_drop(ma_pa_stream* stream) { (void)stream; ++hole_drops; return 0; } |
||||
static void hole_capture(ma_device* device, void* out, const void* in, ma_uint32 frames) { |
||||
(void)device; (void)out; |
||||
assert(in); |
||||
for (ma_uint32 i = 0; i < frames; ++i) assert(((const int16_t*)in)[i] == 0); |
||||
hole_frames += frames; |
||||
} |
||||
static void capture_hole(void) { |
||||
ma_backend backend = ma_backend_null; |
||||
ma_context context; |
||||
ma_device device; |
||||
ma_device_config config = ma_device_config_init(ma_device_type_capture); |
||||
config.capture.format = ma_format_s16; config.capture.channels = 1; config.sampleRate = 48000; |
||||
config.noFixedSizedCallback = MA_TRUE; config.dataCallback = hole_capture; |
||||
assert(ma_context_init(&backend, 1, NULL, &context) == MA_SUCCESS); |
||||
assert(ma_device_init(&context, &config, &device) == MA_SUCCESS); |
||||
context.pulse.pa_stream_peek = (ma_proc)hole_peek; |
||||
context.pulse.pa_stream_drop = (ma_proc)hole_drop; |
||||
observed_holes = hole_frames = hole_drops = 0; |
||||
ma_device__set_state(&device, ma_device_state_started); |
||||
ma_device_on_read__pulse((ma_pa_stream*)&device, 9600, &device); |
||||
assert(observed_holes == 4800 && hole_frames == 4800 && hole_drops == 1); |
||||
ma_device__set_state(&device, ma_device_state_stopped); |
||||
ma_device_uninit(&device); ma_context_uninit(&context); |
||||
puts("PulseAudio capture hole preserves duration and silence PASS"); |
||||
} |
||||
|
||||
#endif |
||||
|
||||
/* Характеризация найденной потери хвоста; после исправления ожидаем два кадра. */ |
||||
static void aec_variable_chunks(void) { |
||||
speex_aec_t* aec = speex_aec_create(48000, 960, 14400, 8); |
||||
int16_t pcm[1920] = {0}; |
||||
assert(aec != NULL); |
||||
speex_aec_feed_playback(aec, pcm, 1200); |
||||
speex_aec_feed_playback(aec, pcm + 1200, 720); |
||||
const int frames = speex_aec_delay_fill(aec); |
||||
assert(frames == 2); |
||||
printf("AEC characterization: input=1920 queued=%d*960; all samples preserved\n", frames); |
||||
speex_aec_destroy(aec); |
||||
} |
||||
|
||||
int main(void) { |
||||
audio_diag_start(); |
||||
starvation(1); starvation(0); |
||||
#ifdef MA_SUPPORT_PULSEAUDIO |
||||
no_progress(); |
||||
bounded_wait(); |
||||
capture_hole(); |
||||
#endif |
||||
aec_variable_chunks(); |
||||
audio_diag_stop(); |
||||
return 0; |
||||
} |
||||
@ -0,0 +1,253 @@
|
||||
#include "audio_diagnostics.h" |
||||
|
||||
#ifdef __linux__ |
||||
#include <algorithm> |
||||
#include <array> |
||||
#include <atomic> |
||||
#include <chrono> |
||||
#include <cstring> |
||||
#include <fstream> |
||||
#include <set> |
||||
#include <map> |
||||
#include <string> |
||||
#include <system_error> |
||||
#include <thread> |
||||
#include <time.h> |
||||
#include <sys/syscall.h> |
||||
#include <unistd.h> |
||||
|
||||
extern "C" { |
||||
#include "../../../lib/debug_config.h" |
||||
} |
||||
|
||||
namespace { |
||||
constexpr uint64_t kCapacity = 8192; |
||||
constexpr uint64_t kSecond = 1000000000; |
||||
const char* const kFaults[] = {"hole", "unfilled", "capture-drop", "no-progress", "backend-underrun", "backend-overflow", "error"}; |
||||
const char* const kStages[] = {"loop", "read", "write", "map", "submit", "duplex", "callback", "feed", "pull"}; |
||||
static_assert(std::atomic<uint64_t>::is_always_lock_free); |
||||
|
||||
struct Event { |
||||
uintptr_t device; |
||||
uint64_t wall, cpu; |
||||
int64_t value, detail; |
||||
int kind, stage; |
||||
long tid; |
||||
char text[192]; |
||||
}; |
||||
struct Slot { std::atomic<uint64_t> sequence{0}; Event event{}; }; |
||||
std::array<Slot, kCapacity> queue; |
||||
std::atomic<uint64_t> writePosition{0}, lost{0}; |
||||
std::atomic<bool> running{false}; |
||||
uint64_t readPosition = 0; |
||||
std::thread reporter; |
||||
|
||||
uint64_t now(clockid_t clock) { |
||||
timespec ts{}; |
||||
clock_gettime(clock, &ts); |
||||
return uint64_t(ts.tv_sec) * kSecond + uint64_t(ts.tv_nsec); |
||||
} |
||||
|
||||
/* Ограниченная MPSC-очередь: producer не ждёт, не выделяет память и не пишет лог. */ |
||||
void publish(const void* device, int kind, int stage, int64_t value, int64_t detail, const char* text) { |
||||
if (!running.load(std::memory_order_relaxed)) return; |
||||
const auto wall = now(CLOCK_MONOTONIC), cpu = now(CLOCK_THREAD_CPUTIME_ID); |
||||
uint64_t pos = writePosition.load(std::memory_order_relaxed); |
||||
for (int attempt = 0; attempt < 4; ++attempt) { |
||||
auto& slot = queue[pos % kCapacity]; |
||||
const auto seq = slot.sequence.load(std::memory_order_acquire); |
||||
if (seq == pos && writePosition.compare_exchange_weak(pos, pos + 1, std::memory_order_relaxed)) { |
||||
auto& e = slot.event; |
||||
e.device = reinterpret_cast<uintptr_t>(device); e.wall = wall; e.cpu = cpu; |
||||
e.kind = kind; e.stage = stage; e.value = value; e.detail = detail; |
||||
e.tid = syscall(SYS_gettid); |
||||
e.text[0] = 0; |
||||
if (text) { |
||||
std::strncpy(e.text, text, sizeof(e.text) - 1); |
||||
e.text[sizeof(e.text) - 1] = 0; |
||||
} |
||||
slot.sequence.store(pos + 1, std::memory_order_release); |
||||
return; |
||||
} |
||||
if (seq < pos) break; // Полное кольцо.
|
||||
pos = writePosition.load(std::memory_order_relaxed); |
||||
} |
||||
lost.fetch_add(1, std::memory_order_relaxed); |
||||
} |
||||
|
||||
bool take(Event& event) { |
||||
auto& slot = queue[readPosition % kCapacity]; |
||||
if (slot.sequence.load(std::memory_order_acquire) != readPosition + 1) return false; |
||||
event = slot.event; |
||||
slot.sequence.store(readPosition + kCapacity, std::memory_order_release); |
||||
++readPosition; |
||||
return true; |
||||
} |
||||
|
||||
struct Stage { |
||||
uint64_t entered = 0, cpu = 0, last = 0, calls = 0, frames = 0, maxGap = 0, maxWork = 0, maxCpu = 0; |
||||
int64_t input = 0, output = 0; |
||||
long tid = 0; |
||||
}; |
||||
struct Device { |
||||
std::string label = "unlabelled"; |
||||
uint64_t id = 0; |
||||
std::array<Stage, AUDIO_DIAG_STAGE_COUNT> stages{}; |
||||
std::array<uint64_t, AUDIO_DIAG_LOG + 1> faults{}; |
||||
}; |
||||
|
||||
void consume(const Event& e, std::map<uintptr_t, Device>& devices) { |
||||
if (e.kind == AUDIO_DIAG_LOG) { |
||||
// Hole учитывается кадрами в read-ветке; строка на каждый hole засоряет лог.
|
||||
if (std::strstr(e.text, "Hole.")) return; |
||||
std::string message(e.text); |
||||
while (!message.empty() && (message.back() == '\n' || message.back() == '\r')) message.pop_back(); |
||||
if (e.value == 1) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "AudioDiag: miniaudio log=%p %s", (void*)e.device, message.c_str()); |
||||
else if (e.value == 2) DEBUG_WARN(DEBUG_CATEGORY_CALL, "AudioDiag: miniaudio log=%p level=%lld %s", |
||||
(void*)e.device, (long long)e.value, message.c_str()); |
||||
else if (e.value == 3) DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "AudioDiag: miniaudio log=%p %s", (void*)e.device, message.c_str()); |
||||
else DEBUG_TRACE(DEBUG_CATEGORY_CALL, "AudioDiag: miniaudio log=%p %s", (void*)e.device, message.c_str()); |
||||
return; |
||||
} |
||||
if (e.kind == AUDIO_DIAG_CLOSE) { |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: close dev=%p mono_ms=%llu", (void*)e.device, (unsigned long long)(e.wall / 1000000)); |
||||
devices.erase(e.device); |
||||
return; |
||||
} |
||||
auto& d = devices[e.device]; |
||||
if (e.kind == AUDIO_DIAG_LABEL) { |
||||
d.label = e.text; d.id = uint64_t(e.value); |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: label dev=%p role=%s id=%016llx", (void*)e.device, e.text, (unsigned long long)d.id); |
||||
} else if (e.kind == AUDIO_DIAG_OPEN) { |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: open dev=%p type=%lld rate=%lld mono_ms=%llu", (void*)e.device, |
||||
(long long)e.value, (long long)e.detail, (unsigned long long)(e.wall / 1000000)); |
||||
} else if (e.kind == AUDIO_DIAG_BACKEND_STARTED) { |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: backend playback started/resumed dev=%p mono_ms=%llu", |
||||
(void*)e.device, (unsigned long long)(e.wall / 1000000)); |
||||
} else if (e.kind == AUDIO_DIAG_NOTIFY) { |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: notification dev=%p type=%lld state=%lld mono_ms=%llu", (void*)e.device, |
||||
(long long)e.value, (long long)e.detail, (unsigned long long)(e.wall / 1000000)); |
||||
} else if (e.kind == AUDIO_DIAG_ENTER && e.stage >= 0 && e.stage < AUDIO_DIAG_STAGE_COUNT) { |
||||
auto& s = d.stages[e.stage]; |
||||
s.input = e.detail; |
||||
if (s.last && e.wall >= s.last) s.maxGap = std::max(s.maxGap, e.wall - s.last); |
||||
s.last = s.entered = e.wall; s.cpu = e.cpu; s.tid = e.tid; |
||||
++s.calls; |
||||
if (e.value > 0) s.frames += uint64_t(e.value); |
||||
} else if (e.kind == AUDIO_DIAG_LEAVE && e.stage >= 0 && e.stage < AUDIO_DIAG_STAGE_COUNT) { |
||||
auto& s = d.stages[e.stage]; |
||||
if (s.entered && s.tid == e.tid && e.wall >= s.entered) { |
||||
s.maxWork = std::max(s.maxWork, e.wall - s.entered); |
||||
s.maxCpu = std::max(s.maxCpu, e.cpu - s.cpu); |
||||
} |
||||
s.output = e.detail; |
||||
s.entered = 0; |
||||
} else if (e.kind >= AUDIO_DIAG_HOLE && e.kind <= AUDIO_DIAG_ERROR) { |
||||
d.faults[e.kind] += uint64_t(std::max<int64_t>(e.value, 1)); |
||||
// Первая аномалия за окно, остальные попадут в сводку.
|
||||
if (d.faults[e.kind] == uint64_t(std::max<int64_t>(e.value, 1))) |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, |
||||
"AudioDiag: fault dev=%p role=%s id=%016llx kind=%s stage=%s value=%lld detail=%lld tid=%ld mono_ms=%llu", |
||||
(void*)e.device, d.label.c_str(), (unsigned long long)d.id, kFaults[e.kind - AUDIO_DIAG_HOLE], |
||||
e.stage >= 0 && e.stage < AUDIO_DIAG_STAGE_COUNT ? kStages[e.stage] : "none", |
||||
(long long)e.value, (long long)e.detail, e.tid, (unsigned long long)(e.wall / 1000000)); |
||||
} |
||||
} |
||||
|
||||
void report(std::map<uintptr_t, Device>& devices, uint64_t wall, double elapsed) { |
||||
const auto dropped = lost.exchange(0, std::memory_order_relaxed); |
||||
if (dropped) DEBUG_WARN(DEBUG_CATEGORY_CALL, "AudioDiag: diagnostic events lost=%llu; interval is incomplete", |
||||
(unsigned long long)dropped); |
||||
std::set<long> stalledThreads; |
||||
for (auto& [key, d] : devices) { |
||||
for (int i = 0; i < AUDIO_DIAG_STAGE_COUNT; ++i) { |
||||
auto& s = d.stages[i]; |
||||
if (!s.last) continue; |
||||
const double pending = s.entered && wall >= s.entered ? (wall - s.entered) / 1e6 : 0; |
||||
const double idle = wall >= s.last ? (wall - s.last) / 1e6 : 0; |
||||
const auto level = s.maxWork > 50000000 || s.maxGap > 50000000 || pending > 50 || idle > 100 |
||||
? DEBUG_LEVEL_WARN : DEBUG_LEVEL_DEBUG; |
||||
if (level == DEBUG_LEVEL_WARN) stalledThreads.insert(s.tid); |
||||
// Без вызова backend-API: reporter никогда не трогает живые ma_device/pa_stream.
|
||||
if (debug_should_output(level, DEBUG_CATEGORY_CALL)) debug_output(level, DEBUG_CATEGORY_CALL, __func__, __FILE__, __LINE__, |
||||
"AudioDiag: dev=%p role=%s id=%016llx stage=%s tid=%ld interval=%.3fs calls=%.1f/s frames=%.0f/s " |
||||
"gap_max=%.2fms work_max=%.2fms cpu_max=%.2fms pending=%.2fms since_enter=%.2fms in_detail=%lld out_detail=%lld mono_ms=%llu", |
||||
(void*)key, d.label.c_str(), (unsigned long long)d.id, kStages[i], s.tid, elapsed, |
||||
s.calls / elapsed, s.frames / elapsed, s.maxGap / 1e6, s.maxWork / 1e6, s.maxCpu / 1e6, |
||||
pending, idle, (long long)s.input, (long long)s.output, (unsigned long long)(wall / 1000000)); |
||||
s.calls = s.frames = s.maxGap = s.maxWork = s.maxCpu = 0; |
||||
} |
||||
if (d.faults[AUDIO_DIAG_HOLE] || d.faults[AUDIO_DIAG_UNFILLED] || d.faults[AUDIO_DIAG_CAPTURE_DROP] || |
||||
d.faults[AUDIO_DIAG_NO_PROGRESS] || d.faults[AUDIO_DIAG_BACKEND_UNDERRUN] || |
||||
d.faults[AUDIO_DIAG_BACKEND_OVERFLOW] || d.faults[AUDIO_DIAG_ERROR]) |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "AudioDiag: dev=%p hole_frames=%llu unfilled_frames=%llu capture_drop_frames=%llu " |
||||
"no_progress=%llu backend_underruns=%llu backend_overflows=%llu errors=%llu", (void*)key, |
||||
(unsigned long long)d.faults[AUDIO_DIAG_HOLE], (unsigned long long)d.faults[AUDIO_DIAG_UNFILLED], |
||||
(unsigned long long)d.faults[AUDIO_DIAG_CAPTURE_DROP], |
||||
(unsigned long long)d.faults[AUDIO_DIAG_NO_PROGRESS], |
||||
(unsigned long long)d.faults[AUDIO_DIAG_BACKEND_UNDERRUN], |
||||
(unsigned long long)d.faults[AUDIO_DIAG_BACKEND_OVERFLOW], (unsigned long long)d.faults[AUDIO_DIAG_ERROR]); |
||||
d.faults.fill(0); |
||||
} |
||||
for (auto tid : stalledThreads) { |
||||
const auto path = std::string("/proc/self/task/") + std::to_string(tid) + "/"; |
||||
std::ifstream waitFile(path + "wchan"), schedFile(path + "schedstat"); |
||||
std::string wait; |
||||
uint64_t cpu = 0, ready = 0, slices = 0; |
||||
if (waitFile >> wait && schedFile >> cpu >> ready >> slices) |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "AudioDiag: thread tid=%ld wchan=%s cpu_total=%.2fms ready_wait_total=%.2fms slices=%llu", |
||||
tid, wait.c_str(), cpu / 1e6, ready / 1e6, (unsigned long long)slices); |
||||
else DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "AudioDiag: thread tid=%ld proc snapshot unavailable (thread may have exited)", tid); |
||||
} |
||||
} |
||||
|
||||
void run() { |
||||
std::map<uintptr_t, Device> devices; |
||||
auto lastReport = now(CLOCK_MONOTONIC); |
||||
for (;;) { |
||||
Event e; |
||||
for (uint64_t n = 0; n < kCapacity && take(e); ++n) consume(e, devices); |
||||
const auto wall = now(CLOCK_MONOTONIC); |
||||
if (wall - lastReport >= kSecond) { report(devices, wall, (wall - lastReport) / double(kSecond)); lastReport = wall; } |
||||
if (!running.load(std::memory_order_acquire)) break; |
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20)); |
||||
} |
||||
report(devices, now(CLOCK_MONOTONIC), 1.0); |
||||
} |
||||
} // namespace
|
||||
|
||||
void audio_diag_start(void) { |
||||
if (running.load()) return; |
||||
writePosition = 0; readPosition = 0; lost = 0; |
||||
for (uint64_t i = 0; i < kCapacity; ++i) queue[i].sequence.store(i, std::memory_order_relaxed); |
||||
running.store(true, std::memory_order_release); |
||||
try { reporter = std::thread(run); } |
||||
catch (const std::system_error& e) { |
||||
running = false; |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "AudioDiag: reporter start failed: %s", e.what()); |
||||
return; |
||||
} |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: started capacity=%llu reporting=1s category=call; timestamps=CLOCK_MONOTONIC", |
||||
(unsigned long long)kCapacity); |
||||
} |
||||
void audio_diag_stop(void) { |
||||
if (!running.exchange(false, std::memory_order_acq_rel)) return; |
||||
reporter.join(); |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: stopped"); |
||||
} |
||||
void audio_diag_event(const void* device, int kind, int stage, int64_t value, int64_t detail) { |
||||
publish(device, kind, stage, value, detail, nullptr); |
||||
} |
||||
void audio_diag_label(const void* device, uint64_t id, const char* name) { |
||||
publish(device, AUDIO_DIAG_LABEL, -1, int64_t(id), 0, name); |
||||
} |
||||
void audio_diag_log(const void* log, unsigned int level, const char* message) { |
||||
publish(log, AUDIO_DIAG_LOG, -1, level, 0, message); |
||||
} |
||||
#else |
||||
void audio_diag_start(void) {} |
||||
void audio_diag_stop(void) {} |
||||
void audio_diag_event(const void*, int, int, int64_t, int64_t) {} |
||||
void audio_diag_label(const void*, uint64_t, const char*) {} |
||||
void audio_diag_log(const void*, unsigned int, const char*) {} |
||||
#endif |
||||
@ -0,0 +1,30 @@
|
||||
#pragma once |
||||
|
||||
#include <stdint.h> |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
/* Только метаданные: PCM и содержимое сообщений не записываются. */ |
||||
enum audio_diag_stage { |
||||
AUDIO_DIAG_LOOP, AUDIO_DIAG_READ, AUDIO_DIAG_WRITE, AUDIO_DIAG_MAP, |
||||
AUDIO_DIAG_SUBMIT, AUDIO_DIAG_DUPLEX, AUDIO_DIAG_CALLBACK, |
||||
AUDIO_DIAG_FEED, AUDIO_DIAG_PULL, AUDIO_DIAG_STAGE_COUNT |
||||
}; |
||||
enum audio_diag_kind { |
||||
AUDIO_DIAG_ENTER, AUDIO_DIAG_LEAVE, AUDIO_DIAG_OPEN, AUDIO_DIAG_CLOSE, |
||||
AUDIO_DIAG_NOTIFY, AUDIO_DIAG_BACKEND_STARTED, AUDIO_DIAG_HOLE, AUDIO_DIAG_UNFILLED, AUDIO_DIAG_CAPTURE_DROP, |
||||
AUDIO_DIAG_NO_PROGRESS, AUDIO_DIAG_BACKEND_UNDERRUN, AUDIO_DIAG_BACKEND_OVERFLOW, AUDIO_DIAG_ERROR, AUDIO_DIAG_LABEL, AUDIO_DIAG_LOG |
||||
}; |
||||
|
||||
/* start/stop — управляющий поток; перед stop все producers должны быть остановлены. */ |
||||
void audio_diag_start(void); |
||||
void audio_diag_stop(void); |
||||
void audio_diag_event(const void* device, int kind, int stage, int64_t value, int64_t detail); |
||||
void audio_diag_label(const void* device, uint64_t id, const char* name); |
||||
void audio_diag_log(const void* log, unsigned int level, const char* message); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
@ -1,2 +1,71 @@
|
||||
#include "audio_diagnostics.h" |
||||
#ifdef __linux__ |
||||
#include <dlfcn.h> |
||||
static void audio_diag_pulse_init(void* device); |
||||
#define MA_DIAGNOSTIC_EVENT(device, kind, stage, value, detail) audio_diag_event(device, kind, stage, value, detail) |
||||
#define MA_DIAGNOSTIC_LOG(log, level, message) audio_diag_log(log, level, message) |
||||
#define MA_DIAGNOSTIC_DEVICE_INIT(device) audio_diag_pulse_init(device) |
||||
#endif |
||||
#define MINIAUDIO_IMPLEMENTATION |
||||
#include "../../../lib/miniaudio.h" |
||||
|
||||
#ifdef __linux__ |
||||
#ifdef MA_SUPPORT_PULSEAUDIO |
||||
static void audio_diag_pulse_underflow(ma_pa_stream* stream, void* userdata) { |
||||
(void)stream; |
||||
audio_diag_event(userdata, AUDIO_DIAG_BACKEND_UNDERRUN, AUDIO_DIAG_WRITE, 1, 0); |
||||
} |
||||
static void audio_diag_pulse_overflow(ma_pa_stream* stream, void* userdata) { |
||||
(void)stream; |
||||
audio_diag_event(userdata, AUDIO_DIAG_BACKEND_OVERFLOW, AUDIO_DIAG_WRITE, 1, 0); |
||||
} |
||||
static void audio_diag_pulse_started(ma_pa_stream* stream, void* userdata) { |
||||
(void)stream; |
||||
audio_diag_event(userdata, AUDIO_DIAG_BACKEND_STARTED, AUDIO_DIAG_WRITE, 1, 0); |
||||
} |
||||
#endif |
||||
|
||||
/* Только init-поток: backend-callbacks остаются короткими наблюдателями. */ |
||||
static void audio_diag_pulse_init(void* device) { |
||||
#ifdef MA_SUPPORT_PULSEAUDIO |
||||
ma_device* dev = (ma_device*)device; |
||||
ma_pa_stream_set_suspended_callback_proc underflow, overflow, started; |
||||
if (dev->pContext->backend != ma_backend_pulseaudio || !dev->pulse.pStreamPlayback) return; |
||||
#ifndef MA_NO_RUNTIME_LINKING |
||||
underflow = (ma_pa_stream_set_suspended_callback_proc)dlsym(dev->pContext->pulse.pulseSO, "pa_stream_set_underflow_callback"); |
||||
overflow = (ma_pa_stream_set_suspended_callback_proc)dlsym(dev->pContext->pulse.pulseSO, "pa_stream_set_overflow_callback"); |
||||
started = (ma_pa_stream_set_suspended_callback_proc)dlsym(dev->pContext->pulse.pulseSO, "pa_stream_set_started_callback"); |
||||
#else |
||||
underflow = pa_stream_set_underflow_callback; |
||||
overflow = pa_stream_set_overflow_callback; |
||||
started = pa_stream_set_started_callback; |
||||
#endif |
||||
if (!underflow || !overflow || !started) { |
||||
audio_diag_log(ma_device_get_log(dev), MA_LOG_LEVEL_WARNING, "PulseAudio xrun/started callbacks unavailable"); |
||||
return; |
||||
} |
||||
underflow((ma_pa_stream*)dev->pulse.pStreamPlayback, audio_diag_pulse_underflow, dev); |
||||
overflow((ma_pa_stream*)dev->pulse.pStreamPlayback, audio_diag_pulse_overflow, dev); |
||||
started((ma_pa_stream*)dev->pulse.pStreamPlayback, audio_diag_pulse_started, dev); |
||||
audio_diag_log(ma_device_get_log(dev), MA_LOG_LEVEL_INFO, "PulseAudio xrun/started observers registered"); |
||||
#else |
||||
(void)device; |
||||
#endif |
||||
} |
||||
#endif |
||||
|
||||
/* Вызывается только владельцем общего контекста (GUI), вне device callbacks. */ |
||||
int audio_context_failed(ma_context* context) { |
||||
#ifdef MA_SUPPORT_PULSEAUDIO |
||||
if (context && context->backend == ma_backend_pulseaudio) { |
||||
int result = ((ma_pa_mainloop_iterate_proc)context->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)context->pulse.pMainLoop, 0, NULL); |
||||
ma_pa_context_state_t state = ((ma_pa_context_get_state_proc)context->pulse.pa_context_get_state)((ma_pa_context*)context->pulse.pPulseContext); |
||||
if (result < 0 || state == MA_PA_CONTEXT_FAILED || state == MA_PA_CONTEXT_TERMINATED) { |
||||
ma_log_postf(ma_context_get_log(context), MA_LOG_LEVEL_ERROR, "[PulseAudio] Shared context failed iterate=%d state=%d errno=%d.\n", |
||||
result, state, ((ma_pa_context_errno_proc)context->pulse.pa_context_errno)((ma_pa_context*)context->pulse.pPulseContext)); |
||||
return 1; |
||||
} |
||||
} |
||||
#endif |
||||
return 0; |
||||
} |
||||
|
||||
Loading…
Reference in new issue