Measure uasync CPU load and display it in desktop and Android GUIs
Sample thread CPU usage without waking idle loops, expose a one-second moving average, and refresh compact GUI indicators at 3 Hz while visible on Android. Add load and bridge regression coverage, streaming notes, and task list cleanup.
This commit is contained in:
1 parent
bcfe7ca4c1
commit
71d19013c0
25 files changed
+692
-587
No files matched your search
@@ -0,0 +1,10 @@
|
||||
Модуль стриминга. Архитектурные нюансы.
|
||||
|
||||
- реализуем в utun отдельным каталогом: src/stream
|
||||
|
||||
- создаем отдельные списки подключений (ncd) для стриминга.
|
||||
главная цель: узлы ищут более оптимальные пути доставки контента от источника до себя.
|
||||
Надо учитывать что узлы могут быть перегружены трафиком. 2 сигнала перегрузки: очередь отправки и поток uasync Не справляется с нагрузкой.
|
||||
надо сделать замер нагрузки на uasync.
|
||||
- узлы учавствующие в стриминге анонсируют себя аналогично как сделано в рации.
|
||||
У модуля свой фоновый опросчик связности и поиска хороших линков - он пробует связь с узлами стриминна кроме мобильных, и обновляет их rtt и флаг прямой доступности.
|
||||
+1
-572
@@ -2,12 +2,7 @@
|
||||
|
||||
Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ.
|
||||
|
||||
## Текущая задача
|
||||
[+] **Group files: удаление непустых каталогов** — 10.10.2026:
|
||||
Один диалог подтверждает удаление всего выделения с содержимым папок. Сначала полный постраничный обход,
|
||||
затем удаление детей раньше родителей; перед удалением папки запрашивается свежая версия после собственных изменений.
|
||||
Проверено на настоящем mf_storage: дерево из 46 объектов, несколько корней, отмена, изменение файла/страницы,
|
||||
появление нового файла перед удалением родителя. Новые файлы не подхватываются повторным обходом.
|
||||
## Текущие задачи
|
||||
|
||||
[ ] **Member files: исчерпание кеша результатов массовых изменений** — 10.10.2026:
|
||||
Воспроизведено test_member_files_browser::recursiveDeletion(server-busy): из 76 объектов удаляются 64,
|
||||
@@ -17,10 +12,6 @@
|
||||
простое вытеснение старых записей нарушает гарантию повторного ответа. После исправления изменить регрессию
|
||||
на успешное удаление более 64 объектов и проверить повторные запросы/потерю ответа.
|
||||
|
||||
[+] **Desktop: текущая вероятность речи на кнопке VAD** — 09.10.2026:
|
||||
Зелёная полоса внизу кнопки отображает 0–100% из того же снимка ядра, что настройки; обновляется до 10Гц.
|
||||
При недоступной оценке остаётся пустая дорожка, в режиме уровня полоса скрыта. Нажатия и удержание сохранены.
|
||||
|
||||
[ ] **Desktop: провалы вероятности VAD при компрессии** — диагностика 09.10.2026:
|
||||
Воспроизведение пользователя: длительная гласная, затем временная нечувствительность; ближе к концу лога несколько повторов.
|
||||
Новый лог 14:32:14–14:34:38: на 14:34:07–18 и 14:34:20–38 все 556 окон с RMS > -30dBFS имеют p < 0.5;
|
||||
@@ -41,107 +32,16 @@
|
||||
Полного провала всей фразы почти до нуля стенд не воспроизвёл; нужен повтор живой речи с новыми логами для связи с AEC/AGC.
|
||||
Алгоритмы обработки сигнала пока не менялись; исправление причины выбирается по результатам повтора.
|
||||
|
||||
[+] **Регрессия Silero с настоящей моделью доступна в desktop CMake** — 09.10.2026:
|
||||
test_silero_vad переведён на актуальный radio_auto_fsm (в autotools ONNX здесь выключен, поэтому старый тест не собирался).
|
||||
Опциональный --probe сравнивает повторения одной речи при разном фоне/усилении и печатает вероятность, gain и clips.
|
||||
|
||||
[+] **Desktop: общий интерфейс настроек активации рации** — 09.10.2026:
|
||||
Удержание 500мс или правая кнопка на VAD/значке уровня открывает мини-блок; короткое нажатие переключает автопередачу.
|
||||
В мини-блоке и вкладке используется один редактор: две кнопки режима, оба порога dBFS, порог/усиление VAD и задержка.
|
||||
Одновременно отображаются RMS, вероятность речи и фактическое усиление VAD; снимок общего ядра обновляется с частотой до 10Гц.
|
||||
Применение явное, закрытие отменяет несохранённый ввод; новая вкладка прокручивается на небольшом экране.
|
||||
Проверки: radio_panel (удержание без toggle, повторное открытие, применение, индикаторы), gui_language, audio_recovery, voice_tx — PASS.
|
||||
|
||||
[+] **Android: текущая оценка VAD и постоянные настройки обоих детекторов** — 09.10.2026:
|
||||
В мини-блоке и вкладке рации одновременно доступны пороги уровня, порог VAD, усиление перед VAD и задержка отключения.
|
||||
Вероятность речи из работающего Silero выводится в процентах и на шкале с отметкой порога; обновление UI — 100мс.
|
||||
Рядом с пределом усиления показан фактический gain отдельного компрессора VAD в dB; выключенный отмечен как 0 dB.
|
||||
Снимок native/JNI помечает оценку недоступной при RX, ошибке inference, разрыве/остановке захвата или возрасте больше 200мс.
|
||||
При выключенной автоматике или режиме уровня UI предлагает включить VAD для измерения вероятности речи.
|
||||
Проверки: voice_tx (вероятность, свежесть, ошибки, RX, reset), radio_member_mute, radio_panel, gui_language — PASS.
|
||||
voice_tx также сравнивает отображаемый gain с эталонным VAD-компрессором для mono/stereo и проверяет выключенный компрессор.
|
||||
APK arm64-v8a/x86_64 собран; новый индикатор на физическом устройстве пока не проверен.
|
||||
|
||||
[+] **Android: автопередача рации по уровню сигнала** — 08.10.2026:
|
||||
режим VAD/уровень, два порога dBFS, живой измеритель из общего radio_audio и prefetch 600мс.
|
||||
Вкладка рации временно держит микрофон для измерения при выключенной автоматике; уход со вкладки/пауза освобождает этот захват.
|
||||
Оба детектора используют прежнее окно foreground/screen/background; PTT имеет приоритет, RX-защита остаётся общей.
|
||||
Настройки сохраняются одним DataStore edit и передаются одним заданием uasync с подтверждением до старта capture.
|
||||
Кнопка панели показывает выбранный режим и сохранённое включение; JNI принимает явный OFF/VAD/LEVEL.
|
||||
Для уровня на кнопке — значок эквалайзера; удержание VAD/уровня открывает мини-блок режима, измерителя и порогов прямо у кнопки.
|
||||
Выбор режима — две кнопки VAD/уровень с областью нажатия от 48 dp, без вложенного выпадающего меню; выбор записывается в лог radio.
|
||||
Добавлены русские/английские строки и диагностика. assembleDebug (arm64-v8a/x86_64), 21 JVM-тест — PASS.
|
||||
На физическом устройстве новый режим пока не проверен.
|
||||
|
||||
[+] **ChatGUI: автопередача рации по уровню сигнала** — 08.10.2026:
|
||||
выбор VAD/уровень, RMS 20мс после AEC до компрессоров, два порога dBFS (-35/-45 по умолчанию),
|
||||
подтверждение 40мс и общая задержка отключения. Измеритель работает при включённой рации даже без автоматики.
|
||||
Общие FSM, prefetch 600мс, приоритет PTT и RX-защита; уровень работает без Silero.
|
||||
INI: radio_auto_enabled, radio_auto_mode (1=VAD, 2=уровень), radio_auto_hangover_ms,
|
||||
radio_level_on_dbfs/radio_level_off_dbfs. Пара проверяется перед сохранением; ядро отключает автоматику
|
||||
при некорректной паре из INI. Общие настройки/API переименованы в Android без изменения его VAD-интерфейса.
|
||||
Проверки: FSM, voice_tx (PCM/Opus/prefetch/RX/AGC), radio_panel (настройки/индикатор/INI),
|
||||
radio_member_mute, voice_audio_io, audio_recovery, gui_language — PASS; ChatGUI собран.
|
||||
|
||||
[+] **Индикация говорящих в списке участников Android** — 07.10.2026:
|
||||
динамик без волн — молчит, с волнами — говорит; красное перечёркивание сохраняется при mute, включая речь.
|
||||
Отдельный набор говорящих активной рации переживает обновления участников и просмотр другого канала;
|
||||
STOP снимает только свой источник, выключение/смена активной рации и stop ядра очищают состояние.
|
||||
Собственный значок использует фактический native TX (PTT/VAD) с привязкой к каналу, сброс после join capture.
|
||||
Добавлены переводы и логи изменений radio. Чистые основная C-сборка и APK — PASS; 16 JVM-тестов — PASS.
|
||||
APK установлен с сохранением данных на Q8 и SM-A525F; приложения запускаются без AndroidRuntime ошибок.
|
||||
|
||||
[ ] **Linux headless CMake: актуализировать зависимости общего ядра** — 07.10.2026:
|
||||
чистая сборка tools/chatgui-android/build_headless останавливается на lib/speex_aec.c:
|
||||
speex/speex_echo.h не найден. utun_setup_sources включает этот исходник, но UTUN_INCLUDE_DIRS не содержит
|
||||
lib/speexdsp/include. Проверить полный состав Speex/voice зависимостей отдельного каркаса;
|
||||
основная Autotools C-сборка и Android NDK-сборка проходят.
|
||||
|
||||
[+] **Динамик участника показывает речь в рации ChatGUI** — 07.10.2026:
|
||||
без волн — молчит, с волнами — говорит, красное перечёркивание — локально заглушён; волны видны и при mute.
|
||||
START/STOP обновляют строки через Qt bridge; собственная передача учитывает PTT, хоткеи и VAD.
|
||||
Обновления списка/RTT сохраняют индикацию; смена канала, выключение рации и stop очищают её.
|
||||
Исправлен сдвиг индексов строк при удалении участника. Добавлены переводы подсказок и логи radio.
|
||||
Чистая сборка desktop, 4 Qt-теста и сетевой test_radio с FIN/таймаутом/отпиской — PASS;
|
||||
полный check.sh — 138 PASS, 0 FAIL, 1 SKIP; proxy/burst/load — PASS. Формы значков проверены визуально.
|
||||
|
||||
[+] **Случайный зашифрованный padding ETCP** — 07.10.2026:
|
||||
keepalive UDP/TCP, UDP PING/PONG и STCP-handshake используют нулевой padding переменной длины.
|
||||
Каждый ETCP_CONN хранит свой диапазон и таймер; отдельный диапазон instance обслуживает пинги без соединения.
|
||||
min=rand(0,30), max=min+rand(32,128), обновление каждые rand(50,300) секунд.
|
||||
Добавлены проверки границ 0/158, эхо-данных, sleep/ACK, независимости и отмены таймеров, ошибок MTU/формата.
|
||||
TCP-пинг не удерживает instance. Полный check.sh: 134 PASS, 0 FAIL, 1 SKIP; proxy/burst/load — PASS.
|
||||
|
||||
[+] **STCP: исключить повтор CTR-потока между подключениями** — 07.10.2026:
|
||||
CTR IV теперь зависит от session_key, направления и соли отправителя из существующего блока handshake.
|
||||
Потоки инициализируются по мере получения солей; шифротекст Ed25519 и нулевого padding меняется.
|
||||
Проверяются повторные и параллельные подключения одной пары, короткие send/recv и подмена соли при том же ECDH.
|
||||
STCP — 14/14; полный check.sh — 134 PASS, 0 FAIL, 1 SKIP; proxy/burst/load — PASS.
|
||||
|
||||
[ ] **REALITY: исправить формат ClientHello и продолжение TLS-потока** — 07.10.2026:
|
||||
supported_groups и signature_algorithms не содержат внутренние двухбайтовые длины списков.
|
||||
После ClientHello/ServerHello идёт STCP без TLS record framing; это распознаётся пассивным наблюдателем.
|
||||
|
||||
[+] **Одна реакция автора и горизонтальный выбор смайликов** — 07.10.2026:
|
||||
выбор в Android и desktop отображается одной горизонтальной прокручиваемой строкой с выделением своего смайлика.
|
||||
Ядро допускает одну реакцию на сообщение от автора; другой смайлик атомарно заменяет прежний, повторный клик отменяет.
|
||||
Многосмайловые состояния отклоняются кодеком, в том числе корректно подписанные; ver и подпись защищают изменения/отмены.
|
||||
По выбору пользователя прежнее состояние 👍+😮 заменено на 👍 новой подписанной версией 3; резервная копия и разовый
|
||||
скрипт сохранены в `/home/user/utun2-backups/single-reaction-20261007`.
|
||||
Local и Q8 обновлены. Тесты кодеков/API/Merkle/сетевого восстановления и 7 Qt-тестов — PASS; APK и desktop собраны.
|
||||
|
||||
[+] **Реакции в нижней строке пузыря в Android и desktop** — 07.10.2026:
|
||||
слева внутри пузыря, включая голосовые/файлы/изображения/видео/опросы, добавлен прозрачный контур белого сердечка
|
||||
с тенью в один физический пиксель во все стороны. Большие сообщения только из эмодзи показываются без кнопки.
|
||||
Сердечко открывает выбор реакции; существующие реакции добавляют/убирают собственный выбор через общий API.
|
||||
Сердечко уменьшено до 20; реакции находятся в общей строке с временем справа. При переносе реакций пузырь
|
||||
увеличивается по высоте, время сохраняется справа в последней строке. Media-метаданные остаются выше неё.
|
||||
Проверены входящие/исходящие сообщения шести типов в узком и широком desktop-окне: нажатия не запускают медиа,
|
||||
drag/right-click не отправляют реакцию, внутри сердечка нет заливки. Проверено изменение высоты строки при
|
||||
добавлении/удалении 16 реакций. Чистые Android/desktop-сборки и 7 Qt-тестов — PASS.
|
||||
Local перезапущен; APK установлен и запущен на Q8, профиль сохранён в `/home/user/utun2-backups/inline-reactions-20261007`.
|
||||
Визуальная проверка Q8 ждёт разблокировки экрана; установка на SM — подключения устройства к ADB.
|
||||
|
||||
[ ] **Завершить проверку реакций и голосов на SM** — 07.10.2026:
|
||||
причина отказа установлена: у записей корней `signed_by=0/NULL` были нулевые/отсутствующие подписи дерева.
|
||||
Проверка корня ключом канала остаётся обязательной; добавлены причины отказа и проверка доверенного Ed25519.
|
||||
@@ -153,49 +53,6 @@
|
||||
Linux/Android/desktop собраны; `./check.sh`: 133 passed, 0 failed, 1 skipped; proxy/full, burst, load и 7 Qt-тестов — PASS.
|
||||
Осталось установить APK на SM (`RZ8R72MG64B`) и проверить реакцию/голос на устройстве: SM отключился от ADB во время работы.
|
||||
|
||||
[+] **Восстановить отправку сообщений SM (Android) → local после обновления** — 07.10.2026:
|
||||
существующие таблицы `msg_*` не имели нового поля `message_id`, INSERT и чтение истории завершались ошибкой.
|
||||
При регистрации канала таблица атомарно перестраивается с вычислением ID из канала, timestamp и подписи;
|
||||
прежние поля сообщений сохраняются, при ошибке вся операция откатывается. Список каналов читается до DDL.
|
||||
`test_db_sync_schema` проверяет сохранение всех полей, одинаковые timestamp, ограничения ID, пустую историю,
|
||||
повторный запуск, отправку после обновления и откат частичного копирования. Проверены копии БД SM и local.
|
||||
Обновлены и запущены SM и local: все 89 сообщений каждого клиента и прежние поля сохранены, SQLite quick_check OK.
|
||||
Резервные копии: `/home/user/utun2-backups/message-id-20261007`. Чистые сборки Linux и Android прошли;
|
||||
`./check.sh`: 133 passed, 0 failed, 1 skipped; proxy/full, burst и load — PASS.
|
||||
|
||||
[+] **Голосования, опросы и реакции в чате** — 06.10.2026, копия `/home/user/utun3`, ветка `feature/chat-feedback`:
|
||||
реализован базовый слой `src/chat/chat_feedback.c/h`: таблица каждого канала,
|
||||
подписи участников, независимые ver для ответа и реакции, сохраняемая отмена,
|
||||
детерминированный конфликт ver, атомарное применение страниц и восстановление индекса.
|
||||
Merkle v3 разделяет MEMBERS/FEEDBACK, курсор записи — 32 байта.
|
||||
Итоговый `./check.sh`: 132 passed, 0 failed, 1 skipped; proxy/full, burst и load также PASS.
|
||||
Чистые сборки desktop и Android assembleDebug прошли; 7 Qt-тестов — PASS.
|
||||
Подключены message_id к БД/API, общий каталог 16 эмодзи, канонические форматы
|
||||
poll/answer/reactions, single/multiple, allow_custom_answer, подсчёт результатов,
|
||||
отложенное получение опроса, CLI, desktop и Android. Собраны desktop и Android APK.
|
||||
Сетевой тест A ↔ B ↔ C прошёл, включая restart C и сохранение отмены.
|
||||
Исправлен запуск начального member_sync после restart чата поверх уже UP-транспорта.
|
||||
Реализация и финальная проверка завершены. APK: app/build/outputs/apk/debug/app-debug.apk
|
||||
внутри tools/chatgui-android; desktop: tools/chatgui/build/vibechat.
|
||||
План продолжения:
|
||||
1. Сохранить индексируемый message_id в msg_<channel>, вернуть его через API сообщений.
|
||||
2. Добавить общий каталог эмодзи и канонические кодеки poll/answer/reactions;
|
||||
single/multiple и allow_custom_answer проверяются ядром.
|
||||
3. Добавить снимок результатов, список голосовавших и собственные состояния;
|
||||
неизвестное сообщение откладывает учёт ответа, получение сообщения обновляет представление.
|
||||
4. Подключить единый командный API к CLI, desktop и Android; команды выполняются в uasync.
|
||||
5. Прогнать проверки кодеков и сетевые сценарии, собрать интерфейсы и выполнить check.sh.
|
||||
|
||||
[+] **tcp_proxy_full: проверить аномальные значения elapsed в отчёте** — 06.10.2026:
|
||||
`/tmp/utun3-check.log` при успешной передаче показывает длительности вроде
|
||||
`basic_1mb: 161949390ms` и `concurrent_20: -1612179507375916304ms`.
|
||||
В `tests/tcp_proxy_full/run_test.sh` длительность считается через `date +%s%3N`;
|
||||
проверить фактический формат времени и арифметику в окружении запуска.
|
||||
Причина установлена: date +%s%3N в окружении отдаёт 19 цифр наносекунд вместо миллисекунд.
|
||||
run_test.sh переведён на Python time.monotonic_ns() // 1000000.
|
||||
Подтверждено финальным check.sh: basic_1mb 179ms, concurrent_20 326ms, idle 1080ms;
|
||||
все результаты передачи и проверки байтов — PASS. Лог: /tmp/utun3-polls-check.log.
|
||||
|
||||
[ ] **rlottie: разбор предупреждений GCC 15 в импортированных исходниках** — 05.10.2026:
|
||||
при сборке `tools/chatgui/rlottie_src` GCC сообщает `-Wuninitialized` в
|
||||
`VArenaAlloc::reset()`: fFirstBlock/fFirstSize/fFirstHeapAllocationSize читаются
|
||||
@@ -205,17 +62,6 @@
|
||||
Лог: `/tmp/utun-rlottie-vendor-build.log`. Сборка библиотеки и подключение её
|
||||
экспортированного CMake-пакета к ChatGUI на Linux прошли; Windows ещё не проверен.
|
||||
|
||||
[+] **ChatGUI Windows: проверка линковки ZXing без -Os** — 05.10.2026:
|
||||
после удаления старых DLL и чистой сборки линкер не находит move-конструктор
|
||||
`std::string` в `libZXing.a(Barcode.cpp.obj)`. ZXing задаёт этому файлу -Os;
|
||||
такая ошибка описана в https://github.com/msys2/MINGW-packages/issues/31015.
|
||||
Для MinGW GCC отключён пофайловый -Os; используются флаги выбранного типа сборки.
|
||||
Проверена генерация команд: при MinGW+GNU Barcode.cpp получает -O3 без -Os,
|
||||
Linux сохраняет прежний -Os. Компиляция Barcode.cpp на Linux прошла.
|
||||
Windows-прогон `tools/chatgui/build.bat` прошёл линковку и дошёл до раскладки DLL;
|
||||
это подтверждено следующим `1.log`. В списке системных DLL раскладки также добавлены
|
||||
`imagehlp.dll` и `bcryptprimitives.dll`, ошибочно считавшиеся отсутствующими зависимостями.
|
||||
|
||||
[ ] **ChatGUI Windows: подтверждение обновления runtime компилятора** — 05.10.2026:
|
||||
`cc-probe.log`: cc1 запускается из корня, но в `tools/chatgui/build` завершается
|
||||
с `0xC0000139`; локальные DLL GCC/math/runtime отличаются от UCRT64.
|
||||
@@ -231,124 +77,11 @@
|
||||
Runtime GCC обновляется отдельно; для остальных DLL нужно продумать обновление
|
||||
с сохранением приоритета собственной сборки FFmpeg и проверить запуск приложения.
|
||||
|
||||
[+] **Открытие HTTP/HTTPS в сообщениях Android и Qt** — 04.10.2026:
|
||||
общая обработка ссылок по диапазонам текста; веб-адреса открываются системным
|
||||
обработчиком, `utun://` сохраняет JOIN и короткое отображение PM-приглашений.
|
||||
Учтены Unicode, IPv6, скобки, пунктуация, параметры, fragment и emoji в адресах.
|
||||
Qt использует одинаковый layout для отрисовки, размеров, нажатия и выделения.
|
||||
Ошибки открытия переведены и логируются. Общие 15 случаев прошли в Android и Qt;
|
||||
проверки нажатия/drag/copy и приглашений прошли. Чистые сборки Qt и APK — PASS.
|
||||
`check.sh`: 124 passed, 0 failed, 1 skipped; proxy/burst/load — PASS.
|
||||
APK установлен на Q8, проверено системное открытие HTTP и HTTPS через ACTION_VIEW,
|
||||
приложение запущено. Логи: `/tmp/message-links-check.log`,
|
||||
`/tmp/message-links-qt-final-tests.log`, `/tmp/message-links-android-tests.log`.
|
||||
Контракт: `doc/message_links.md`.
|
||||
|
||||
[+] **Заголовок PM-приглашения и короткое отображение ссылки** — 04.10.2026:
|
||||
`ct=invite` содержит зашифрованные название группы и полную ссылку; GUI получает
|
||||
название через `tags.group_name` и добавляет заголовок из перевода интерфейса.
|
||||
Android и Qt отображают ссылку одной строкой с многоточием. Нажатие и копирование
|
||||
используют полный адрес: долгое нажатие в Android, выделение строки и Ctrl+C в Qt.
|
||||
Срок действия ключа — 10 минут. Проверена доставка названия с Unicode, кавычками
|
||||
и переносом строки, JOIN и история; Qt проверяет разные ширины, нажатие на конец
|
||||
сокращённой ссылки и копирование. Чистые сборки ядра, Qt и APK прошли.
|
||||
`check.sh`: 124 passed, 0 failed, 1 skipped; proxy/burst/load — PASS.
|
||||
Qt `test_invite_links` и `test_emoji_playback` — PASS. APK установлен на Q8,
|
||||
приложение запущено. Логи: `/tmp/pm-invite-title-check.log`,
|
||||
`/tmp/pm-invite-title-e2e-final.log`. Контракт: `doc/pm_invites.md`.
|
||||
|
||||
[+] **История группы после PM-приглашения** — 04.10.2026: регистрация `db_sync`
|
||||
создаёт локальное состояние с блокировкой обмена; история запускается после
|
||||
успешного member_sync через READY-пиров конкретной группы. Подписка на переходы
|
||||
READY сбрасывает обмен при DOWN/REINIT/LEAVE и возобновляет его при восстановлении.
|
||||
Обе роли имеют 15-секундный таймаут; ошибки отправки и отказ пира разрешают повтор.
|
||||
JOIN_INFO_RESP применяет участника через транзакционный member_sync, обновляя
|
||||
Merkle-индекс вместе с моделью. ASAN также выявил и помог исправить использование
|
||||
временного content_type после выхода из области видимости при выдаче JSON истории.
|
||||
Проверены пять сообщений до приглашения, оба порядка ID, потеря первого обмена,
|
||||
восстановление READY на общем транспорте и последующая доставка новых сообщений.
|
||||
Эти сценарии, ownership и services прошли под ASAN. Финальный check.sh:
|
||||
124 passed, 0 failed, 1 skipped; интеграционные proxy/burst/load — PASS.
|
||||
Чистые сборки ядра, Qt и APK прошли. На Q8 после обновления
|
||||
существующий `g2` получил все 5 старых сообщений без повторного JOIN;
|
||||
SYNC_DONE подтвердил совпадение хеша, SQLite quick_check — ok.
|
||||
Диагностика: `/tmp/pm-history-q8-final.log`, `/tmp/pm-history-asan-invite.log`,
|
||||
`/tmp/pm-history-asan-timeout.log`, `/tmp/pm-history-final-check.log`.
|
||||
Контракт: `src/chat/db_sync_arch.md`.
|
||||
|
||||
[+] **Android: диагностика несрабатывания VAD** — 03.10.2026: в `debug` добавлены
|
||||
решения по каждому изменению состояния и сводки PCM/AGC/модели раз в секунду;
|
||||
`debug=trace` показывает каждое окно 32мс. Логи различают сброс подтверждения,
|
||||
чужой RX, закрытое окно Android, задержки inference и отложенный BEGIN.
|
||||
Регрессии radio_vad, Silero, radio_audio и voice_tx прошли. На Samsung в 19:36:23
|
||||
подтверждён отказ после истечения 15-минутного окна: `reason=window_expired`,
|
||||
`requested=true screen=true capture=false nativeVad=false`; ручной PTT работает.
|
||||
Кнопка на панели показывает сохранённое включение, а не фактическую активность VAD.
|
||||
Лог: `/tmp/vad-sm-current.log`. Исправление таймера и индикации — следующая запись.
|
||||
|
||||
[+] **Android: индикация и фоновый таймер VAD** — 03.10.2026: кнопка показывает
|
||||
фактическую активность захвата, гаснет при его остановке; повторное включение
|
||||
открывает новое окно. 15-минутный таймер расходуется только в фоне, при видимом
|
||||
приложении сбрасывается: после каждого сворачивания доступны полные 15 минут.
|
||||
После истечения возврат в приложение возобновляет VAD; погасший экран по-прежнему
|
||||
останавливает микрофон, включение экрана открывает новое окно.
|
||||
`tests/test_radio_vad_window.kt` проверяет 2 часа в foreground, сброс таймера при
|
||||
возврате, полное фоновое окно, границу истечения, экран и сохранение окна при перенастройке.
|
||||
На Samsung проверены фактические lifecycle-переходы и выключенная кнопка после
|
||||
остановки захвата: `/tmp/vad-background-sm-transitions.log`. Чистая сборка APK прошла,
|
||||
Samsung SM-A525F и Q8 обновлены; на Samsung восстановлены прежний канал и активный VAD.
|
||||
Уточнение: при возвращении в приложение таймер теперь полностью сбрасывается,
|
||||
соответствующая регрессия прошла. Новая чистая сборка — `/tmp/vad-reset-android-build.log`,
|
||||
установлена на оба телефона. Повторная проверка lifecycle на Samsung после этого
|
||||
обновления не выполнена: телефон заблокирован; правило сброса проверено тестом с искусственным временем.
|
||||
|
||||
[ ] **Android: время жизни config provider** — 03.10.2026: `utun_bridge_init()`
|
||||
передаёт адрес локального `utun_config_provider_t` в `utun_config_set_provider()`,
|
||||
который хранит указатель. После возврата из init он недействителен; перед
|
||||
использованием `cfg_get_*` обеспечить постоянное хранение provider.
|
||||
|
||||
[+] **DM: надёжная доставка сообщений, первый этап pm.txt** — 01.10.2026:
|
||||
атомарные история/seq/outbox, подписанные точные квитанции после commit,
|
||||
live BGP-маршрут и durable очередь на суперузле с проверкой подписанной роли.
|
||||
Повторы, out-of-order, rollback БД, неверные ACK и поздний PUT проверены;
|
||||
40 offline-сообщений доставлены после restart источника, первый offline DM —
|
||||
после restart суперузла при выключенном источнике. Копии удалены по квитанции.
|
||||
Подготовка ciphertext медиа использует ключ target и независимые nonce порций;
|
||||
проверены границы 32 КиБ, пустые файлы, неверные ключи/UUID/hash и повреждения.
|
||||
Clean-сборка ядра/Qt, test_dm, test_dm_e2e и полный check прошли:
|
||||
113 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root); proxy/burst/load PASS.
|
||||
Текущие гарантии и оставшаяся интеграция: `doc/dm_arch.md`.
|
||||
|
||||
[+] **DM: сетевая доставка и pending-медиа по pm.txt** — 01.10.2026:
|
||||
отдельные owners dm_media/dm_mailbox_media и файловый transfer с CM handle,
|
||||
подписанные метаданные и независимая квитанция файла после durable приёма.
|
||||
Прямая доставка или S→M custody, restart S/M при выключенном A, доставка B,
|
||||
повтор удаления до DELETE_ACK; поздние STORE/STORED не возрождают копии.
|
||||
TTL M по умолчанию 7 дней, резервирование квоты, отмена transfer и stop workers.
|
||||
Копирование, шифрование, хеширование, проверка и durable файловые операции —
|
||||
в media_async; отправка по 8 КиБ через call_soon. Тест 8 МиБ/0,5 с сохраняет
|
||||
heartbeat uasync (максимальный интервал 10,3 мс в проверенном прогоне).
|
||||
Qt/Android: оригинальные файловые вложения, сохранение и статусы доставки;
|
||||
PM-снимки из ядра асинхронные. Чистые сборки Qt и Android APK прошли.
|
||||
test_dm_media/test_dm_worker, rollback/quota/cancel и полный check:
|
||||
115 PASS, 0 FAIL, 1 SKIP; proxy/burst/load PASS. GUI на устройствах не проверялся.
|
||||
Протокол, владение и проверки: `doc/dm_arch.md`.
|
||||
|
||||
[+] **DM: голосовые/видео и общий GUI вложений** — 01.10.2026:
|
||||
тип FILE/VOICE/VIDEO и метаданные зашифрованы ключом target; в transport остаётся
|
||||
подписанный descriptor, без новых ключей и канального протокола в DM.
|
||||
Общие attachment/voice_file/attachment_send: Opus, waveform, копирование и видео
|
||||
готовятся в workers; completion через reserved post вместо polling. PCM передаётся
|
||||
без копии; адресат закреплён до записи/выбора. Отмена освобождает собственные файлы.
|
||||
Android: ConversationComposer/ConversationMessages для каналов и PM, CameraX MP4,
|
||||
IO для URI/метаданных; Qt: общий InputBar/delegate, FFmpeg, фоновые превью/Opus.
|
||||
Подготовка адресована беседе и UUID задачи; статусы доставки показаны для всех типов.
|
||||
test_attachment/test_attachment_send проверяют кодек, формат, владение и отмену;
|
||||
test_dm_media — voice напрямую и encrypted video metadata после offline restart.
|
||||
Чистые сборки ядра/Qt/APK, 117 PASS / 0 FAIL / 1 SKIP в check.sh; proxy/burst/load PASS.
|
||||
Qt CTest: 7 PASS, включая повреждения Opus и отмену decoder. APK не устанавливался,
|
||||
интерактивная проверка устройств не выполнялась. Подробности: `doc/dm_arch.md`.
|
||||
|
||||
[ ] **uasync: ложные ERROR об утечке перед cleanup** — 01.10.2026:
|
||||
test_dm_e2e и существующий test_conn_mgr проходят, но uasync_destroy()
|
||||
пишет Timer leak до освобождения отменённых heap/immediate таймеров.
|
||||
@@ -356,31 +89,6 @@
|
||||
предварительные счётчики в DEBUG; ERROR оставить для реального расхождения
|
||||
после cleanup, отдельно показывать активные и отменённые таймеры.
|
||||
|
||||
[+] **Статус подключения звонка в Qt и Android** — фактический путь router:
|
||||
прямое / reverse / indirect / нет подключения. Учитывается BGP fallback;
|
||||
события только при изменении, общий таймер 1с, сброс при завершении.
|
||||
Проверены смена путей настоящего звонка, медиа, ASAN, Qt, Android APK и полный check.
|
||||
|
||||
[+] **Единицы RTT в окне звонка** — `call_post_stats()` переводит
|
||||
`ETCP_CONN.rtt_last` из 0.1мс в миллисекунды до отправки CALL_STATS.
|
||||
Исправление общее для Qt и Android. DEBUG-категория `debug` раз в секунду
|
||||
показывает исходный RTT, RTT в мс, транспортного соседа и метрики аудио.
|
||||
|
||||
[+] **Восстановление путей conn_mgr на протяжении жизни handle** — начальный
|
||||
TIMEOUT отделён от срока цикла проб. Поздний обрыв запускает новый цикл;
|
||||
рабочий резерв и NCD сохраняются, при отсутствии резерва INDIRECT стартует сразу.
|
||||
Циклы ограничены 15с, паузы 1→2→4→8→15с; новая версия узла или локальные сокеты
|
||||
обходят паузу. Прямой READY останавливает поиск и получает приоритет.
|
||||
Исходящие request_id и входящие сроки независимы; устаревшие ответы игнорируются.
|
||||
UP/DOWN отражают общую доступность, TIMEOUT приходит один раз до первого UP.
|
||||
Последний close отменяет восстановление, включая закрытие из callback.
|
||||
Проверены десять сетевых сценариев с реальными данными: поздний обрыв,
|
||||
повторные циклы, смена адресов/сокетов, потеря маршрута посредника, старый ACK,
|
||||
начальный TIMEOUT с поздним UP и close. ASAN без ошибок; собраны Qt и Android APK.
|
||||
Полный check: 113 PASS, 0 FAIL, 1 SKIP; пропущенный auto_socket_dynamic отдельно
|
||||
прошёл с root в network namespace. Интеграционные proxy, burst и load прошли.
|
||||
Алгоритм: `src/routing_layer/conn_mgr_doc.md`.
|
||||
|
||||
[ ] **Общий лимит check.sh после clean** — 01.10.2026: расчёт по времени предыдущего
|
||||
прогона дал 205с вместе с полной сборкой и оборвал проверку после успешных тестов
|
||||
до `test_uasync_socket_race`. Повторный запуск на готовой сборке с большим лимитом
|
||||
@@ -406,26 +114,6 @@
|
||||
capture/playback, фактической частоты callback и PCM кадров за интервал.
|
||||
Счётчики сбрасываются до старта устройства; нужна повторная запись с новой сборкой.
|
||||
|
||||
[+] **Ложный сигнал потери звука при запуске звонка** — 01.10.2026, звонок
|
||||
`bc3efadcac629947`: Qt запускает аудиоколлбэки в 01:54:56.933, включает gap tone в
|
||||
01:54:57.415 и выключает в 01:54:58.072 при первом воспроизведении PCM.
|
||||
На SM между ACCEPT и первым захваченным кадром проходит около 1.15с;
|
||||
часы SM отстают примерно на 1.23с. Qt стабильно обслуживает 100 callback/с,
|
||||
RTT около 1мс. Исправлено: до первого PCM начальное ожидание проходит в тишине,
|
||||
последующий перебой включает тон через 500мс. Стартовый сбой по-прежнему
|
||||
ограничен существующим watchdog отсутствия медиа. Регрессия проверяет ожидание
|
||||
1.5с, начальное накопление, настоящий перебой, неполный блок, новый звонок и завершение.
|
||||
Ускорение запуска Android: router.start занимает около 775мс; система сообщает
|
||||
длительное удержание AudioManager.updateAudioPortCache. Ещё около 370мс проходит
|
||||
до запуска capture worker. Гудки теперь останавливаются до startAudio;
|
||||
router использует снимок устройств с обновлением по событиям и явной переоценке,
|
||||
capture запускается до подготовки/старта playback. Ошибка playback останавливает capture.
|
||||
Добавлены длительности отдельных стадий, первого capture и PCM. Ядро, Qt и APK
|
||||
собраны после clean; три аудиотеста прошли. Выигрыш на SM требует замера в новом звонке.
|
||||
В звонке Qt `180f046ef22defc2` первый PCM пришёл через 1245мс без стартового gap tone.
|
||||
Полный check вне песочницы: 113 PASS, 0 FAIL, 1 SKIP; auto_socket_dynamic отдельно
|
||||
прошёл с root в private netns. Интеграционные proxy, burst и load прошли.
|
||||
|
||||
[ ] **Оставшаяся задержка запуска аудио SM** — после обновления 01.10.2026,
|
||||
звонок `2b9ad4e21a54d8ba`: стартового gap tone нет на обеих сторонах; Qt
|
||||
стабильно работает около 100 callback/с, max_idle=15.3мс, буфер 55мс, RTT=2мс.
|
||||
@@ -456,96 +144,6 @@
|
||||
`stress_sessions` иногда печатают огромные положительные/отрицательные миллисекунды,
|
||||
хотя итоговая скорость и длительность передачи выглядят нормально. Проверить расчёт вывода.
|
||||
|
||||
[+] **SpeexDSP AEC (эхоподавление) в lib/** — акустическое эхоподавление звонка.
|
||||
Вендорен SpeexDSP (mdf/fftwrap/kiss_fft, 3-clause BSD) в `lib/speexdsp/`,
|
||||
обёртка `lib/speex_aec.c/h` (C-API, FLOATING_POINT + USE_KISS_FFT, без внешних
|
||||
зависимостей). Собственная линия задержки рендера на `delay_frames` кадров +
|
||||
синхронный `speex_echo_cancellation` (встроенный буфер SpeexDSP фиксирован в
|
||||
2 кадра и для Android не годится); дрейф потоков — через переполнение/недозаполнение
|
||||
линии (drop/dup + passthrough). Категория лога `aec`.
|
||||
Ядро: `call_audio_set_aec_enabled/delay_frames`, AEC в feed_pcm (захват→encode)
|
||||
и pull_pcm (рендер-референс), lifecycle в start/release. Desktop — настройка GUI
|
||||
`aec_enabled` (`[chatserver]`, читается в `call_audio_start`). Android — включается
|
||||
только на спикерфоне (`route==SPEAKER`, live через JNI `callAudioSetAec`),
|
||||
задержка меряется `AudioTrack/AudioRecord.getTimestamp`; `CallAudioEngine.playLoop`
|
||||
переведён на absolute-deadline (устранён систематический дрейф). Тест
|
||||
`test_speex_aec` (подавление эха 25.5 дБ, сохранность речи 0.956, delay-line, NULL-гарды).
|
||||
Сборки: autotools + chatgui CMake + Android CMake/APK.
|
||||
|
||||
[+] **Silero VAD в lib/** — добавлен детектор голосовой активности (Silero VAD v5,
|
||||
стриминговая ONNX-модель + обёртка `lib/silero_vad.c/h` над ONNX Runtime C API).
|
||||
API: `silero_vad_create/create_default/destroy/reset/process/process_pcm16`
|
||||
(окно 512 сэмплов @16 кГц, вероятность речи [0..1], рекуррентное состояние).
|
||||
Модель вшита в бинарник (`silero_vad_model.inc`, `create_default`), файл не нужен.
|
||||
Категория лога `vad`. Модуль под `#ifdef HAVE_SILERO_VAD` — без onnxruntime
|
||||
компилируется в стабы, поэтому GLOB-сборки (chatgui/Android) не ломаются.
|
||||
Сборка: `--with-silero-vad` (autotools), `-DHAVE_SILERO_VAD` + libonnxruntime
|
||||
(chatgui CMake, Android NDK). `make check`: 111/0/1.
|
||||
|
||||
[+] **Авто-PTT рации по VAD** — сделано. Отдельный режим `radio_auto_enabled` (ручной
|
||||
PTT не меняется, при зажатой кнопке — override). Сервисный слой `radio_audio.c`:
|
||||
ресемплер 48k→16k моно (3-tap box + 3:1, стерео микшируется), накопитель окон
|
||||
512, фильтр-гистерезис + машина состояний (`src/radio/radio_auto.h`, чистый,
|
||||
юнит-тестируемый): старт при подтверждении 2 окон (~64мс) выше порога
|
||||
`radio_vad_threshold`, стоп при тишине `radio_auto_hangover_ms` (200мс) или чужом
|
||||
разговоре (busy = RX-кадр <200мс). Перед BEGIN сохраняется до 600мс непереданного PCM;
|
||||
prefetch отправляется одной задачей перед live без повторов и задержки на стороне отправителя.
|
||||
VAD использует отдельный компрессор с собственной историей усиления. Desktop `radio_audio_engine.cpp` и Android
|
||||
`RadioAudioEngine.kt` фидят PCM постоянно в VAD-режиме; JNI `radioVadMode()`.
|
||||
Тест `test_radio_vad` (ресемплер + FSM). Android: onnxruntime AAR 1.23.2 в
|
||||
`jniLibs/` + `cpp/onnxruntime/include` + `-DHAVE_SILERO_VAD` в CMake.
|
||||
Android: настройки VAD (включение, порог, задержка после тишины); микрофон в PTT
|
||||
создаётся только для передачи. PTT запускается при касании, после отпускания
|
||||
передаёт ещё 250мс; повторное касание отменяет завершение (двойной тап без FIN).
|
||||
VAD работает 15 минут при горящем экране;
|
||||
SCREEN_OFF освобождает захват, SCREEN_ON автоматически открывает новое окно.
|
||||
RX остаётся активен; TX освобождается отдельно через `radio_audio_capture_stop()`.
|
||||
|
||||
[+] **Отдельный компрессор перед VAD рации** — 03.10.2026:
|
||||
копия PCM 48кГц/20мс проходит через общий audio_compressor до ресемплера и Silero.
|
||||
Настройки общие с TX, кроме `radio_vad_compressor_max_gain_db` (15дБ по умолчанию,
|
||||
1..60дБ); включение также общее. Состояния TX/VAD независимы, prefetch хранит исходный PCM.
|
||||
Выключенный компрессор не добавляет ожидания блока; разрыв/новый захват сбрасывает AGC и Silero.
|
||||
Android: отдельный ползунок, DataStore/INI/runtime; диагностика `radio` показывает
|
||||
параметры, input/output RMS, фактический gain и клиппинг. Диапазоны max_gain согласованы с библиотекой.
|
||||
test_voice_tx: точный вход Silero, байтовое совпадение Opus/prefetch, mono/stereo,
|
||||
6/30дБ, переменные PCM-чанки и reset. Настоящий Silero распознаёт речь при амплитуде
|
||||
0.03 на тихом фоне, останавливается после речи; повторный reset воспроизводит результат.
|
||||
Чистые сборки ядра/Qt/APK, целевые тесты и полный check.sh прошли:
|
||||
124 PASS / 0 FAIL / 1 SKIP, proxy/burst/load PASS; Qt CTest 2/2, AGC 479/479.
|
||||
APK с последующим переносом переключателя VAD установлен на Samsung SM_A525F.
|
||||
|
||||
[+] **Включение VAD на панели рации Android** — 03.10.2026:
|
||||
кнопка VAD расположена слева от правой кнопки плавающего PTT; состояние читается из DataStore Flow.
|
||||
Действие в ViewModel сохраняет настройку, обновляет ядро, запрашивает разрешения и перенастраивает захват.
|
||||
Переключатель удалён из настроек; порог речи, задержка после тишины и max gain оставлены.
|
||||
Чистые сборки ядра/Qt/APK прошли, APK установлен на Samsung; UI настроек проверен через uiautomator.
|
||||
|
||||
[+] **Верхняя панель рации и VAD в desktop ChatGUI** — 03.10.2026:
|
||||
строка под шапкой канала: меню микрофона/выхода, состояние, переключатель VAD слева от PTT.
|
||||
PTT перенесена с сообщений в панель; подсветка показывает фактический TX, включая автоматический VAD.
|
||||
Настройки рации: вероятность речи, задержка тишины, включение AGC и независимые max gain TX/VAD.
|
||||
Общий стек сохраняет prefetch 600мс; настройки применяются в uasync с подтверждением в GUI.
|
||||
Перезапускается только capture, RX/output сохраняются; удерживаемые кнопка/хоткей восстанавливаются.
|
||||
INI сохраняется атомарно; ошибка микрофона/детектора видна на панели, ручной PTT доступен без VAD.
|
||||
Запрос настроек принадлежит reserved uasync task и освобождается также при shutdown до исполнения.
|
||||
Исправлено сохранение Audio без посещения страницы: устройства и аудиопараметры предварительно загружаются.
|
||||
Чистые сборки ядра/Qt и Qt CTest 16/16; test_radio_panel проверяет Qt-кнопки и настоящие audio workers
|
||||
с null backend: auto-TX, 30 кадров prefetch, manual/hotkey, capture recovery, INI restart и отмену запроса.
|
||||
Полный check.sh: 124 PASS / 0 FAIL / 1 SKIP, proxy/burst/load PASS; настоящий Silero распознаёт тихую речь.
|
||||
Логи: `/tmp/desktop-vad-qt-verified-tests.log`, `/tmp/desktop-vad-core-check-unrestricted.log`.
|
||||
|
||||
[+] **PTT кнопками громкости на панели рации Android** — 03.10.2026:
|
||||
переключатель слева от VAD, включённое состояние подсвечено; панель и настройки используют общий DataStore Flow.
|
||||
Значок: динамик по центру, плюс сверху, минус снизу; проверен на панели Samsung после чистых сборок ядра/Qt/APK.
|
||||
Сохранение обновляет быстрый кэш после записи; ошибки записи логируются без изменения состояния переключателя.
|
||||
Экранная кнопка, оверлей и кнопки громкости имеют независимые владельцы ручной передачи.
|
||||
Учёт отдельных volume-клавиш защищает от повторов и посторонних клавиш; потеря фокуса/выключение опции отменяет volume-PTT.
|
||||
Чистые сборки ядра/Qt/APK прошли, 12 сценариев владельцев PTT проверены отдельным Kotlin-прогоном.
|
||||
APK установлен на Samsung SM_A525F: обе volume-кнопки запускают запись, отпускание завершает её с хвостом 250мс.
|
||||
В отдельном канале без других участников проверены переключатель и сохранение выключенного состояния после restart.
|
||||
Логи: `/tmp/android-volume-ptt-final-device.log`, `/tmp/android-volume-ptt-apk-final-build.log`.
|
||||
|
||||
[ ] **Linux ONNX Runtime: повторная регистрация схем при запуске теста** — 03.10.2026:
|
||||
test_silero_vad при загрузке модели печатает серии `Schema error: ... already registered`.
|
||||
Инференс, речь, тишина и reset проходят; отдельно проверить состав и загрузку Linux-библиотек ONNX.
|
||||
@@ -556,35 +154,10 @@
|
||||
огромные или отрицательные миллисекунды (например, -1611883348002232012ms).
|
||||
Отдельно проверить формат/аргументы вывода длительности; `/tmp/vad-compressor-check.log`.
|
||||
|
||||
[+] **chatgui (Qt) не собирался: C++ void* в utun_node.cpp** — сделано. Причина:
|
||||
`proxy_protocol.h` (C-заголовок, включается в C++ `utun_node.cpp`) делал
|
||||
`e->dgram = u_malloc(...)` и `struct proxy_flow* f = arg;` без явных приведений —
|
||||
в C++ нет неявного `void*`→T*. Добавлены `(uint8_t*)`/`(struct proxy_flow*)`
|
||||
(валидны и в C). `vibechat` собирается (47МБ, с вшитой моделью + onnxruntime).
|
||||
|
||||
[+] **test_auto_socket_dynamic: root-регрессия и нагрузка** — изолированный namespace,
|
||||
корректные маршруты, двусторонний поток через backpressure, строгие seq/payload,
|
||||
пределы TX/RX, остановка при обрыве и drain после восстановления, баланс ресурсов.
|
||||
Исправлены привязка TCP к интерфейсу, удаление TCP моделей/линков/портов из БД,
|
||||
sender receive window при selective ACK, timestamp overflow и нулевой jitter.
|
||||
ASan/UBSan/LSan: PASS; дополнительно исправлены невыровненные метки аллокаторов
|
||||
и PID-порты в test_etcp_connect. [Разбор и логи](etcp_full_suite_findings.md).
|
||||
|
||||
[ ] **debug_parse_config: формат и индексы категорий** — parser ожидает `:`, документация
|
||||
описывает `=`; преобразование битовой маски осталось после перехода enum на индексы.
|
||||
[Разбор](etcp_full_suite_findings.md).
|
||||
|
||||
[+] **Q8/Android не подключается к локальному чатгуи (LAN)** — сделано. Причина: тип сокета
|
||||
в Android-конфиге был захардкожен `type=public`, из-за чего `sock_match` ставил
|
||||
`has_priv=0` и не создавал линки к приватным (LAN) адресам пиров (`create_links … → 0 links`).
|
||||
Исправлено: (1) `ip_is_public()` вынесен в `lib/platform_compat` (единый классификатор
|
||||
IPv4 с корректным ntohl); (2) в `auto_socket_reconcile_config` (Android-режим) тип сокета
|
||||
выставляется по адресу (public→PUBLIC, приватный/link-local→NAT), конфиг `type` игнорируется;
|
||||
(3) `sock_collect_views` больше не смотрит на `type` из конфига: `has_priv` — по классификации
|
||||
локального адреса, `has_inet=1` (исходящие считаем доступными), `is_eim` — по NAT-детекции
|
||||
(hairpin). `is_local_subnet` упрощён до `!ip_is_public()`. Тест `test_sock_match` обновлён
|
||||
(+ `ip_is_public` кейсы). `check.sh`: 86 passed / 1 failed / 1 skipped.
|
||||
|
||||
[ ] **DM: прямой p2p чат с любым пользователем группы** — отдельная DM-подсистема.
|
||||
ТЗ и архитектура: `/doc/dm_arch.md`. Статус: реализовано (dm_core/dm_mailbox/dm_crypto),
|
||||
тесты `test_dm` (крипто) и `test_dm_e2e` (интеграция). Остались задачи ниже.
|
||||
@@ -596,123 +169,11 @@
|
||||
Android: переключение источника (трубка/громкая/гарнитура).
|
||||
Архитектура: `/doc/chat_call_arch.md`.
|
||||
|
||||
[+] **Сигналинг + релей медиа** — сделано. `call_proto.h` (svc `ETCP_RT_ID_CALL
|
||||
0x36`, subcmd INVITE/RINGING/ACCEPT/DECLINE/BUSY/CANCEL/HANGUP/MEDIA),
|
||||
`call.c` (машина состояний, реестр сессий, conn_mgr, ring-timeout 45с,
|
||||
watchdog no-traffic 20с, потеря соединения → end, события
|
||||
CHAT_EVT_CALL_* 34–40), `DEBUG_CATEGORY_CALL 32`. `test_call` (2 инстанса,
|
||||
happy path + decline + media-релей). Приём медиа — через `call_set_media_recv_cb`.
|
||||
[+] **Headless API** — сделано. Команды control-сокета `call_start/accept/decline/
|
||||
hangup` + события `call_incoming/ringing/accepted/ended/declined` (JSON);
|
||||
аудио-сокет `call/call_headless.c` (TCP, бинарный фрейм
|
||||
`[type:1][call_id:8][len:2][data]`, HELLO/FRAME), конфиг `call_audio_bind`.
|
||||
Разрыв conn_mgr — отложенно 500мс (`CALL_TEARDOWN_DELAY_TB`) после ENDED,
|
||||
чтобы HANGUP успел долететь; защита от stale-handle через conn-status.
|
||||
Тесты: `test_call` (сигналинг+медиа), `test_call_headless` (полный e2e через
|
||||
control+audio сокеты).
|
||||
[+] **Аудио-контур (time-stretch в сервисном слое)** — сделано. `src/call/audio_jitter.cpp`
|
||||
(SoundTouch, C API `ajb_*`: адаптивный джиттер-буфер ≤1000мс, target 60мс,
|
||||
tempo 1.0→1.6x — догон live без потери данных при сетевой паузе),
|
||||
`src/call/call_tones.c` (глитч при сталле >500мс + тон завершения),
|
||||
`src/call/call_audio.c` (Opus 48kHz/20мс + jitter + тоны + pull API финального PCM).
|
||||
`call.c`: `call_audio_ops` (on_media c seq/ts, get_stats) + событие
|
||||
`CHAT_EVT_CALL_STATS` раз в 1с [rtt_ms, buffer_ms, tempo_x100].
|
||||
Desktop `call_audio_engine.cpp` — только miniaudio I/O (получает финальный PCM).
|
||||
Тест `test_audio_jitter` (ёмкость/догон/стационар). Демон остаётся C-only.
|
||||
[+] **Android** — сделано. JNI (`utun_bridge_call_*`/`nativeCall*`), AudioRecord/Track
|
||||
(CallAudioEngine), роутинг источника (CallAudioRouter: EARPIECE/SPEAKER/HEADSET,
|
||||
BT SCO), CallScreen + IncomingCallDialog + RingtonePlayer + IncomingCallManager,
|
||||
`libutun_lite/call_audio.c` (собственный аудио-контур Opus 48kHz/20мс).
|
||||
[+] **Единый голосовой стек (voice stack) + SoundTouch на Android** — сделано.
|
||||
Унификация аудио-контура звонка: `libutun_voice` (статическая C++-либа с
|
||||
C-интерфейсом `call_audio.h` = SoundTouch + `voice_jitter.cpp` + `call_audio.c`
|
||||
+ `call_tones.c`), собирается через `src/call/voice_sources.cmake` и линкуется
|
||||
desktop/Android/headless-CLI. Джиттер-буфер держит КОДИРОВАННЫЕ (Opus) кадры,
|
||||
декодирует только на pull (непосредственно перед воспроизведением) — память ~48x
|
||||
меньше и uasync-поток разгружен. Android переведён с push-FIFO (Kotlin ArrayDeque)
|
||||
на pull-модель (`nativeCallAudioPull`), своего джиттера больше нет → убрана
|
||||
асимметрия задержки (desktop↔Android). Headless-аудио-сокет отдаёт готовый PCM
|
||||
(декод Opus→PCM, без тактирования; клиент сам задаёт темп). Диагностика:
|
||||
`CALL_STATS` расширен [dropped, underruns] (раз в 1с), счётчики headless.
|
||||
Удалены `audio_jitter.cpp` и `libutun_lite/call_audio.c`. Тест `test_voice_jitter`
|
||||
(ёмкость/догон/стационар). Android — `ANDROID_STL=c++_static`.
|
||||
[ ] **DM-звонки** (fallback `group_id==0`) — не сделано: `call_is_online`/`call_start`
|
||||
требуют реальную группу (`topo_groups_find`), для DM (group_id==0) маршрута нет.
|
||||
|
||||
## Звонок: две фазы исходящего вызова (индикация + гудки)
|
||||
|
||||
[+] **Две фазы вызова** — сделано. Исходящий звонок разбит на 2 фазы с индикацией
|
||||
в окне вызова и разными гудками:
|
||||
- фаза 1 «Пытаемся связаться…» (`OUTGOING` — INVITE/маршрут) — гудок «соединение»;
|
||||
- фаза 2 «Звоним…» (`RINGING` — у пира идёт вызов) — длинные гудки (ringback).
|
||||
Desktop: `CallWindow::Ringing` + `SoundManager` loop `call_connecting`/`call_ringing`
|
||||
(MP3 в qrc), переключение по `onCallRinging`, остановка по accepted/ended/error;
|
||||
добавлена обработка `GUI_EVT_CALL_ERROR` (40) — раньше при «пир офлайн» окно/гудок
|
||||
зависали навсегда. Android: `CallRingback` (MediaPlayer loop из res/raw),
|
||||
подписи `OUTGOING`/`RINGING` в CallScreen + ActiveCallBanner.
|
||||
Звуковые события добавлены в SoundSettingsPage (call_connecting/call_ringing).
|
||||
|
||||
[+] **Надёжность отмены вызова (кодограмма + watchdog)** — сделано. Отмена вызова
|
||||
(HANGUP/CANCEL) шлётся один раз, без ретрансмита, и при флапе маршрута (RST/epoch,
|
||||
прямой TCP к мобиле не поднимается — только релей) терялась: вызываемый висел в
|
||||
RINGING вечно и отвечал BUSY на все новые INVITE. Исправлено моделью «сердцебиения»:
|
||||
- вызывающий шлёт кодограмму INVITE каждые 300мс весь период вызова (OUTGOING+RINGING);
|
||||
- у вызываемого watchdog 2с — нет кодограммы → вызов сорван (`CALL_REASON_CANCEL`);
|
||||
- завершающие кодограммы без ретрансмиссий (потерю покрывает watchdog);
|
||||
- запоздалые кодограммы вызова при разговоре (CONNECTING/ACTIVE) игнорируются.
|
||||
`CALL_INVITE_RETRY_TB=3000`, `CALL_RING_DEAD_TB=20000`, `CALL_NO_ROUTE_MAX_STRIKES=20`.
|
||||
|
||||
## Рефакторинг: per-instance chat/dm (однопоточный test_dm_e2e)
|
||||
|
||||
Сделано (глобальные синглтоны переведены на `UTUN_INSTANCE`):
|
||||
- `chat_setting` → `inst->chat_settings` (`struct chat_setting_state`, state-level API для парсера конфига + instance-level для runtime).
|
||||
- `chat_event` → `inst->chat_event_handler` (обработчик теперь получает `inst`).
|
||||
- `chat_core` (`g_cc`) → `inst->chat_core` (`struct chat_core_ctx*`).
|
||||
- `chat_sync` (`g_cs`) → `inst->chat_sync`.
|
||||
- `member_sync` callback-списки (`g_props_cbks`/`g_apply_cbks`) → в `chat_core_ctx`.
|
||||
- `chat_join` (`g_keys`) → в `chat_core_ctx`.
|
||||
- `chat_msg` media-счётчики/backfill → в `chat_core_ctx`.
|
||||
- `dm_core` (`g_dm`) → `inst->dm`; `dm_mailbox` (`g_mb`) → `inst->dm_mailbox`.
|
||||
- `chat_headless_control` (`g_hc`) → `inst->headless`.
|
||||
- `chat_whisper` — сигнатуры `init/available/trigger` принимают `inst`.
|
||||
- API протащено через `inst` (chat_core_*/dm_*), обновлены call-сайты в src/ (headless, topo, media_delivery, auto_socket, config_parser).
|
||||
- `test_dm_e2e.c` переписан на однопоточный: один `uasync`, три `utun_instance` (A,B,C), master state-machine, без fork. Фаза 2 (B offline) — `utun_instance_destroy(B1)` + пересоздание B2.
|
||||
- Попутно исправлен 1-байтовый overflow в `dm_mailbox.c: mb_route_send` (`u_malloc(1+body_len)` → `1+1+body_len`).
|
||||
|
||||
Статус сборки: `make -C src` (libutun.a + бинарник utun) и `make -C tests` — OK.
|
||||
|
||||
### Осталось (по порядку)
|
||||
|
||||
[+] **test_dm_e2e: зависание в utun_instance_destroy(B1)** (фаза P_B_OFFLINE) — сделано.
|
||||
Причин было несколько (не одна):
|
||||
1. `lib/u_async.c`: если ближайший таймер уже истёк на входе в `uasync_poll`,
|
||||
`get_next_timeout` возвращал {0,0}, `timeout_ms=-1` → `epoll_wait(-1)` блокировался
|
||||
навсегда, а `process_timeouts` (вызывается только после epoll_wait) не запускался.
|
||||
Фикс: `if (timeout_ms == -1 && heap не пуст) timeout_ms = 0`.
|
||||
2. `routing_layer/etcp_router.c`: `router_send_conn` с group_id=0 (глобальная
|
||||
маршрутизация DM/mailbox) не находил маршрут (`topo_groups_find(0)==NULL`).
|
||||
Фикс: fallback на `instance_find_conn(inst, remote)` при group==NULL.
|
||||
3. `dm/dm_core.c`: `dm_on_conn_status` PULL-ил только беседы с подключившимся пиром;
|
||||
B2 подключается к storage (C), а беседа — с A. Фикс: на подъём любого соединения
|
||||
PULL для всех бесед, чей пир не подключён напрямую.
|
||||
4. `dm/dm_mailbox.c`: не было ACK после PULL_RESP → storage не чистил dm_mail.
|
||||
Фикс: PULL_RESP теперь несёт sender, получатель шлёт MB_SUBCMD_ACK.
|
||||
5. `utun_instance.c`: `dm_core_init` ставил deliver-cb до `dm_mailbox_init` →
|
||||
mailbox не находился, deliver-cb не регистрировался. Фикс: mailbox init раньше core.
|
||||
`test_dm_e2e` проходит (несколько прогонов), `test_dm` тоже.
|
||||
|
||||
[+] **chatgui-android: переезд на per-instance API** — сделано.
|
||||
`instance_lite.c/h` (+`instance_lite_get_instance()`), `jni_bridge.c`, `standby.c`,
|
||||
`headless_control.c`, `voice_recorder.c`, `attachment_sender.c`, `video_sender.c`,
|
||||
`photo_sender.c` — все call-сайты переведены на `inst`. `libutun_lite` компилируется,
|
||||
`jni_bridge.c` — синтакс-чисто. (headless-линковка сломана ПРЕДЫДУЩЕ — instance_lite.c
|
||||
ссылается на jni_bridge-функции, не включённые в headless-сборку; отдельная задача.)
|
||||
|
||||
[+] **test_chat_join: segfault** (регресс рефакторинга) — сделано.
|
||||
`chat_join` хранил `join_keys` в `chat_core_ctx` (CC(inst)), а тест использует chat_join
|
||||
без chat_core → NULL-deref. Фикс: `join_keys` вынесен в `UTUN_INSTANCE.join_keys`
|
||||
(у chat_join собственный init/destroy).
|
||||
|
||||
[ ] **BGP-гонка: дропнутый REQUEST_TABLE** (отдельная задача, как договорились).
|
||||
|
||||
**Место:** `topo_group_new_conn()` в `src/routing_layer/topo_group.c:571`. Дедуп-ветка
|
||||
@@ -759,44 +220,12 @@
|
||||
(не пытаться соединяться с узлами, для которых нет `[client]`/ncd-конфигурации,
|
||||
либо обрабатывать неудачу без спама).
|
||||
|
||||
[+] **Обновить GUI call-сайты `tools/chatgui` (Qt, desktop)** — сделано.
|
||||
- В `chat_core.h` вынесены trampoline-структуры из `.c`: `update_my_name_arg`,
|
||||
`save_ui_state_arg`, `chat_setting_arg` (были в chat_profile.c/chat_core.c).
|
||||
- `gui_bridge.h/impl`: `gui_bridge_set_inst()`/`gui_bridge_get_inst()` (+`g_inst`).
|
||||
- `utun_node.cpp`: `gui_bridge_set_inst(m_instance)`, `chat_event_set_handler(m_instance,…)`,
|
||||
`chat_core_sync_my_addresses(m_instance)`.
|
||||
- Обновлены ~45 call-сайтов в 16 файлах (`mainwindow`, `messagelist`, `messagedelegate`,
|
||||
`memberlistmodel`, `memberpropsdialog`, `accountlist`, `invitedialog`, `inviteby`,
|
||||
`joindialog`, `settingsdialog`, `channelsettingsdialog`, `soundsettingspage`,
|
||||
`audiodevicesettingspage`, `storagesettingspage`, `statuspage`, `connmonitorwindow`).
|
||||
- Сборка `cmake --build . --target vibechat` — OK.
|
||||
Попутно: `chat_sync_connect_from_invite` писал `cs->pending_*` на вызывающем (GUI) потоке —
|
||||
перенёс в `cm_invite_trampoline` (uasync-поток), убрав гонку; заодно исправил утечку
|
||||
`inv->addrs_data` и добавил null-check `cs` в трамплине.
|
||||
|
||||
[+] **Прогон всех тестов** — сделано. `make clean && make -j4` OK; все тесты проходят
|
||||
(кроме `test_auto_socket_dynamic` — пропуск «requires root»). `test_dm_e2e` стабилен
|
||||
в нескольких прогонах. В `test_dm_e2e` при teardown остаётся timer-leak (~68 узлов,
|
||||
`router_no_route`), не влияет на результат — стоит разобрать отдельно.
|
||||
|
||||
[ ] **lwip/proxy: утечка pcb в FIN_WAIT_2** — `tcp_shutdown(pcb,0,1)` (half-close по FIN от
|
||||
exit) не ставит `TF_RXCLOSED`, поэтому таймаут FIN_WAIT_2 в `tcp_slowtmr` не срабатывает:
|
||||
если браузер никогда не пришлёт FIN, pcb навсегда остаётся в FIN_WAIT_2 (утечка, не crash).
|
||||
Двойной free по TIME_WAIT уже закрыт (detach по локальному закрытию + guard abort).
|
||||
|
||||
## Открытые флаки
|
||||
[+] **test_chat_join_e2e** — сделано. Причина: ложный критерий успеха джойнера J.
|
||||
`wait_group_started()` проверял `senders_list` непуст в предположении «senders_list
|
||||
непуст ⟺ J получил JOIN_READY». После коммита b483335c `tgi_to_channel_cb`
|
||||
(topo_group_invite.c) при `NCD_EVENT_UP` стал вызывать `topo_group_new_conn` →
|
||||
conn попадает в `senders_list` сразу после установки TCP/UDP-соединения, ДО
|
||||
завершения хендшейка (JOIN_INFO_REQ→RESP→REQUEST→READY). J выходил «OK» раньше,
|
||||
чем A получал JOIN_REQUEST → A не подписывал J → `wait_signed` на A/C падал
|
||||
(`get_sign_rc=-1`). Фикс: критерий J — событие `CHAT_EVT_CONNECT_RESULT(result=0)`
|
||||
(JOIN_READY), ловится через `chat_event_set_handler`; `wait_group_started` удалён.
|
||||
Это также убрало гонку «C вышел раньше, чем J синкает свой рекорд» (C выходит сразу
|
||||
после `wait_signed`, не дожидаясь merkle-синка J). 5 прогонов стабильно 3/3.
|
||||
|
||||
[ ] **test_etcp_reconnect** — флаки под параллельной нагрузкой `make check -j4`:
|
||||
phase 4 (reconnect после server restart) таймаутит (`sent=480 recv=0`), при одиночном
|
||||
запуске стабильно `TEST PASSED`. Наш код (chat_join/chat_sync) его не трогает.
|
||||
+136
-13
@@ -1012,11 +1012,123 @@ static void process_epoll_events(struct UASYNC* ua, struct epoll_event* events,
|
||||
}
|
||||
#endif
|
||||
|
||||
// Instance version
|
||||
void uasync_poll(struct UASYNC* ua, int timeout_tb) {
|
||||
if (!ua) return;
|
||||
if (!ua->sockets || !ua->timeout_heap) return;
|
||||
// CPU-часы читаются только в потоке владельца, включая системное время этого потока.
|
||||
static int uasync_thread_cpu_ns(uint64_t* value) {
|
||||
#ifdef _WIN32
|
||||
FILETIME created, exited, kernel, user;
|
||||
if (!GetThreadTimes(GetCurrentThread(), &created, &exited, &kernel, &user)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "uasync CPU clock failed: error=%lu", (unsigned long)GetLastError());
|
||||
return -1;
|
||||
}
|
||||
uint64_t kernel_ticks = ((uint64_t)kernel.dwHighDateTime << 32) | kernel.dwLowDateTime;
|
||||
uint64_t user_ticks = ((uint64_t)user.dwHighDateTime << 32) | user.dwLowDateTime;
|
||||
*value = (kernel_ticks + user_ticks) * 100;
|
||||
#else
|
||||
struct timespec ts;
|
||||
if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "uasync CPU clock failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
*value = (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define UASYNC_LOAD_PERIOD_US 100000ULL
|
||||
#define UASYNC_LOAD_WINDOW_US 1000000ULL
|
||||
|
||||
static void uasync_update_load(struct UASYNC* ua) {
|
||||
if (ua->load_state < 0) return;
|
||||
uint64_t now = get_time_us();
|
||||
if (ua->load_state && now < ua->load_next_us) return;
|
||||
uint64_t cpu_ns;
|
||||
if (uasync_thread_cpu_ns(&cpu_ns) < 0) {
|
||||
ua->load_state = -1; ua->load.valid = 0;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "uasync CPU load disabled: ua=%p", (void*)ua);
|
||||
return;
|
||||
}
|
||||
if (!ua->load_state) {
|
||||
ua->load_state = 1; ua->load_previous_us = now; ua->load_previous_cpu_ns = cpu_ns;
|
||||
ua->load_next_us = now + UASYNC_LOAD_PERIOD_US; ua->load_log_us = now + UASYNC_LOAD_WINDOW_US;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "uasync CPU load started: ua=%p period=100ms window=1000ms", (void*)ua);
|
||||
return;
|
||||
}
|
||||
if (cpu_ns < ua->load_previous_cpu_ns || now <= ua->load_previous_us) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "uasync CPU load clock regression: ua=%p wall=%llu->%llu cpu=%llu->%llu",
|
||||
(void*)ua, (unsigned long long)ua->load_previous_us, (unsigned long long)now,
|
||||
(unsigned long long)ua->load_previous_cpu_ns, (unsigned long long)cpu_ns);
|
||||
ua->load_state = -1; ua->load.valid = 0;
|
||||
return;
|
||||
}
|
||||
uint64_t elapsed = now - ua->load_previous_us, delta_cpu = cpu_ns - ua->load_previous_cpu_ns;
|
||||
uint64_t late = now - ua->load_next_us;
|
||||
if (late >= UASYNC_LOAD_PERIOD_US && now - ua->last_poll_exit_us >= UASYNC_LOAD_PERIOD_US) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "uasync CPU sample delayed: ua=%p interval=%lluus late=%lluus missed=%llu",
|
||||
(void*)ua, (unsigned long long)elapsed, (unsigned long long)late,
|
||||
(unsigned long long)(late / UASYNC_LOAD_PERIOD_US));
|
||||
}
|
||||
unsigned int capacity = sizeof(ua->load_history) / sizeof(ua->load_history[0]);
|
||||
ua->load_history[ua->load_head] = (struct uasync_load_interval){ua->load_previous_us, now, delta_cpu};
|
||||
ua->load_head = (ua->load_head + 1) % capacity;
|
||||
if (ua->load_count < capacity) ua->load_count++;
|
||||
uint64_t cutoff = now > UASYNC_LOAD_WINDOW_US ? now - UASYNC_LOAD_WINDOW_US : 0;
|
||||
uint64_t window_us = 0;
|
||||
double window_cpu_ns = 0;
|
||||
for (unsigned int i = 0; i < ua->load_count; i++) {
|
||||
const struct uasync_load_interval* sample = &ua->load_history[i];
|
||||
if (sample->end_us <= cutoff) continue;
|
||||
uint64_t start = sample->start_us > cutoff ? sample->start_us : cutoff;
|
||||
uint64_t overlap = sample->end_us - start;
|
||||
window_us += overlap;
|
||||
window_cpu_ns += (double)sample->cpu_ns * overlap / (sample->end_us - sample->start_us);
|
||||
}
|
||||
ua->load = (struct uasync_load){
|
||||
.current_percent = (double)delta_cpu / elapsed / 10.0,
|
||||
.average_percent = window_cpu_ns / window_us / 10.0,
|
||||
.sample_time_us = now, .interval_us = elapsed, .window_us = window_us, .valid = 1
|
||||
};
|
||||
// Разница точности CPU/реальных часов может дать небольшое превышение 100%.
|
||||
if (ua->load.current_percent > 100.0) ua->load.current_percent = 100.0;
|
||||
if (ua->load.average_percent > 100.0) ua->load.average_percent = 100.0;
|
||||
ua->load_previous_us = now; ua->load_previous_cpu_ns = cpu_ns;
|
||||
ua->load_next_us += (late / UASYNC_LOAD_PERIOD_US + 1) * UASYNC_LOAD_PERIOD_US;
|
||||
if (now >= ua->load_log_us) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "uasync CPU load: ua=%p current=%.2f%% average=%.2f%% interval=%lluus window=%lluus late=%lluus",
|
||||
(void*)ua, ua->load.current_percent, ua->load.average_percent, (unsigned long long)elapsed,
|
||||
(unsigned long long)window_us, (unsigned long long)late);
|
||||
ua->load_log_us = now + UASYNC_LOAD_WINDOW_US;
|
||||
}
|
||||
}
|
||||
|
||||
// Секунда непрерывного ожидания полностью вытесняет прежнюю нагрузку из окна.
|
||||
static void uasync_load_after_wait(struct UASYNC* ua, uint64_t wait_start_us) {
|
||||
uint64_t now = get_time_us();
|
||||
ua->last_poll_exit_us = now;
|
||||
if (ua->load_state <= 0 || now - wait_start_us < UASYNC_LOAD_WINDOW_US) return;
|
||||
uint64_t cpu_ns;
|
||||
if (uasync_thread_cpu_ns(&cpu_ns) < 0) { ua->load_state = -1; ua->load.valid = 0; return; }
|
||||
ua->load_history[0] = (struct uasync_load_interval){now - UASYNC_LOAD_WINDOW_US, now, 0};
|
||||
ua->load_head = 1; ua->load_count = 1;
|
||||
ua->load_previous_us = now; ua->load_previous_cpu_ns = cpu_ns;
|
||||
ua->load_next_us = now + UASYNC_LOAD_PERIOD_US;
|
||||
ua->load = (struct uasync_load){.sample_time_us = now, .interval_us = now - wait_start_us,
|
||||
.window_us = UASYNC_LOAD_WINDOW_US, .valid = 1};
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "uasync CPU load: ua=%p current=0%% average=0%% idle=%lluus",
|
||||
(void*)ua, (unsigned long long)(now - wait_start_us));
|
||||
ua->load_log_us = now + UASYNC_LOAD_WINDOW_US;
|
||||
}
|
||||
|
||||
err_t uasync_get_load(struct UASYNC* ua, struct uasync_load* load) {
|
||||
if (!ua || !load) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "uasync_get_load: invalid argument ua=%p load=%p", (void*)ua, (void*)load);
|
||||
return ERR_FAIL;
|
||||
}
|
||||
uasync_update_load(ua);
|
||||
*load = ua->load;
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
static void uasync_poll_once(struct UASYNC* ua, int timeout_tb) {
|
||||
process_immediate_queue(ua);
|
||||
|
||||
uint64_t now_us_poll_entry = get_time_us();
|
||||
@@ -1050,9 +1162,10 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) {
|
||||
struct epoll_event events[64]; // Stack-allocated array for events
|
||||
int max_events = 64;
|
||||
|
||||
uint64_t wait_start_us = get_time_us();
|
||||
int ret = epoll_wait(ua->epoll_fd, events, max_events, timeout_ms);
|
||||
int saved_errno = errno;
|
||||
ua->last_poll_exit_us = get_time_us();
|
||||
uasync_load_after_wait(ua, wait_start_us);
|
||||
if (ret < 0) {
|
||||
if (saved_errno == EINTR) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "epoll_wait→EINTR");
|
||||
@@ -1087,6 +1200,7 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) {
|
||||
|
||||
// If no sockets to poll, just wait and process timeouts
|
||||
if (total_fds == 0) {
|
||||
uint64_t wait_start_us = get_time_us();
|
||||
if (timeout_ms > 0) {
|
||||
#ifdef _WIN32
|
||||
Sleep(timeout_ms);
|
||||
@@ -1095,7 +1209,7 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) {
|
||||
nanosleep(&ts, NULL);
|
||||
#endif
|
||||
}
|
||||
ua->last_poll_exit_us = get_time_us();
|
||||
uasync_load_after_wait(ua, wait_start_us);
|
||||
process_timeouts(ua);
|
||||
return;
|
||||
}
|
||||
@@ -1138,11 +1252,13 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) {
|
||||
tv.tv_sec = timeout_ms / 1000;
|
||||
tv.tv_usec = (timeout_ms % 1000) * 1000;
|
||||
|
||||
uint64_t wait_start_us = get_time_us();
|
||||
int ret = select((int)max_fd + 1, &read_fds, &write_fds, &except_fds, timeout_ms < 0 ? NULL : &tv);
|
||||
ua->last_poll_exit_us = get_time_us();
|
||||
int saved_error = ret < 0 ? WSAGetLastError() : 0;
|
||||
uasync_load_after_wait(ua, wait_start_us);
|
||||
|
||||
if (ret < 0) {
|
||||
int err = WSAGetLastError();
|
||||
int err = saved_error;
|
||||
if (err != WSAEINTR) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "select failed: %d", err);
|
||||
}
|
||||
@@ -1229,9 +1345,10 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) {
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t wait_start_us = get_time_us();
|
||||
int ret = poll(ua->poll_fds, ua->poll_fds_count, timeout_ms);
|
||||
int saved_errno = errno;
|
||||
ua->last_poll_exit_us = get_time_us();
|
||||
uasync_load_after_wait(ua, wait_start_us);
|
||||
if (ret < 0) {
|
||||
if (saved_errno == EINTR) {
|
||||
return;
|
||||
@@ -1344,7 +1461,12 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) {
|
||||
}
|
||||
|
||||
|
||||
// Put this near the top of u_async.c, after includes and before uasync_create
|
||||
void uasync_poll(struct UASYNC* ua, int timeout_tb) {
|
||||
if (!ua || !ua->sockets || !ua->timeout_heap) return;
|
||||
uasync_update_load(ua);
|
||||
uasync_poll_once(ua, timeout_tb);
|
||||
uasync_update_load(ua);
|
||||
}
|
||||
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -1793,7 +1915,8 @@ void uasync_stop(struct UASYNC* ua) {
|
||||
}
|
||||
|
||||
void uasync_mainloop(struct UASYNC* ua) {
|
||||
while (!__atomic_load_n(&ua->stop, __ATOMIC_ACQUIRE)) {
|
||||
uasync_poll(ua, -1);
|
||||
}
|
||||
if (!ua) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "uasync_mainloop: null instance"); return; }
|
||||
while (!__atomic_load_n(&ua->stop, __ATOMIC_ACQUIRE)) {
|
||||
uasync_poll(ua, -1);
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,22 @@ typedef void (*uasync_post_callback_t)(void* user_arg);
|
||||
|
||||
struct timeout_node; // Forward declaration
|
||||
|
||||
/* CPU одного потока: 100% = одно ядро. При работе цикла/запросе снимок обновляется раз в 100 мс, без пробуждений.
|
||||
* До первого замера valid=0; window_us растёт до секунды. При задержке замера
|
||||
* вклад граничного интервала в среднее оценивается пропорционально времени. */
|
||||
struct uasync_load {
|
||||
double current_percent;
|
||||
double average_percent;
|
||||
uint64_t sample_time_us;
|
||||
uint64_t interval_us;
|
||||
uint64_t window_us;
|
||||
int valid;
|
||||
};
|
||||
|
||||
struct uasync_load_interval {
|
||||
uint64_t start_us, end_us, cpu_ns;
|
||||
};
|
||||
|
||||
struct posted_task {
|
||||
uasync_post_callback_t callback;
|
||||
void* arg;
|
||||
@@ -79,6 +95,11 @@ struct UASYNC {
|
||||
volatile sig_atomic_t stop;
|
||||
volatile sig_atomic_t running; // 0=shutdown mode (API rejected), 1=full init done
|
||||
uint64_t last_poll_exit_us;
|
||||
struct uasync_load load;
|
||||
struct uasync_load_interval load_history[12]; // 10 периодов + граничные интервалы при задержках
|
||||
uint64_t load_previous_us, load_previous_cpu_ns, load_next_us, load_log_us;
|
||||
unsigned int load_head, load_count;
|
||||
int load_state; // 0=не начат, 1=работает, -1=ошибка часов
|
||||
};
|
||||
|
||||
// Type definitions
|
||||
@@ -137,6 +158,9 @@ void uasync_stop(struct UASYNC* ua);
|
||||
// Debug statistics
|
||||
void uasync_get_stats(struct UASYNC* ua, size_t* timer_alloc, size_t* timer_free, size_t* socket_alloc, size_t* socket_free);
|
||||
|
||||
// Актуализирует снимок по сроку 100 мс. Только в потоке uasync; наружу передавать копию через события.
|
||||
err_t uasync_get_load(struct UASYNC* ua, struct uasync_load* load);
|
||||
|
||||
// Handle — кодированный индекс; сохраняется при расширении массива сокетов.
|
||||
int uasync_lookup_socket(struct UASYNC* ua, int fd, void** socket_id);
|
||||
|
||||
|
||||
@@ -164,8 +164,35 @@ uint64_t now_us = get_time_us(); // микросекунды (для burst-и
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `uasync_get_stats(ua, ...)` | Получить счётчики аллокаций/освобождений таймеров и сокетов. |
|
||||
| `uasync_get_load(ua, &load)` | Актуализировать по сроку 100 мс и скопировать снимок CPU потока: текущий процент, среднее за секунду, время замера, длительность интервала и окна, valid. Только в потоке uasync. |
|
||||
| `uasync_print_resources(ua, prefix)` | Вывести в лог все активные таймеры (имя, оставшееся время) и сокеты. Вызывается также в `uasync_destroy` перед очисткой. |
|
||||
|
||||
CPU измеряется с первого `uasync_poll` или `uasync_get_load`: по сроку 100 мс читается CPU-время текущего потока
|
||||
(`CLOCK_THREAD_CPUTIME_ID` на POSIX, user+kernel `GetThreadTimes` на Windows). 100% означает одно полностью занятое ядро.
|
||||
Учитывается вся работа этого потока между замерами, включая код между вызовами poll; CPU других потоков не учитывается.
|
||||
Проценты вычисляются по фактической длительности интервала: `100 * cpu_ns / (elapsed_us * 1000)`.
|
||||
Реальное время берётся из `get_time_us` (Linux: `CLOCK_BOOTTIME`, включая suspend; Windows: QPC).
|
||||
|
||||
Сроки измерения идут по сетке 100 мс без накопления дрейфа. Проверка выполняется при работе цикла и при запросе нагрузки.
|
||||
Измеритель не создаёт таймеров, не ограничивает ожидание событий и не будит спящий поток.
|
||||
После непрерывного ожидания не менее секунды весь старый интервал нагрузки вытеснен: текущий процент и среднее равны 0%.
|
||||
Это фиксируется сразу после выхода из ожидания, до callbacks; CPU последующих обработчиков учитывается с новой точки отсчёта.
|
||||
История хранится внутри `UASYNC`, без аллокаций. Частые запросы не создают лишних отсчётов.
|
||||
Среднее ограничено последней секундой; вклад пересекающего границу интервала оценивается пропорционально перекрытию.
|
||||
До первого замера `valid=0`, затем окно растёт до секунды. Долгий callback задерживает публикацию;
|
||||
пропущенные отсчёты не восстанавливаются, текущий процент относится ко всему фактическому интервалу.
|
||||
При большом перерыве вне ожидания среднее внутри этого интервала является оценкой по его общей загрузке.
|
||||
Снимок содержит `sample_time_us`, чтобы потребитель мог показать возраст данных.
|
||||
Ошибка CPU-часов логируется и отключает измеритель (`valid=0`), сохраняя работу цикла событий.
|
||||
В `sys=debug` раз в секунду выводятся проценты, длительность интервала/окна и опоздание;
|
||||
пропуск полного периода из-за долгой обработки выводится как WARN. Статус чата и headless-команда `status` показывают CPU и среднее.
|
||||
|
||||
GUI показывает округлённое секундное среднее 0–100% поверх полосы с градиентом зелёный → жёлтый → красный:
|
||||
desktop — рядом с заголовком каналов, Android — рядом с версией в списке групп.
|
||||
Запрос `chat_core_collect_cpu_load_trampoline` возвращает короткое событие `CHAT_EVT_CPU_LOAD` (`valid:1, average_percent:1`).
|
||||
GUI опрашивает ядро каждые 333 мс, не более одного незавершённого запроса. Android опрашивает только пока экран списка групп RESUMED;
|
||||
при паузе или уходе с экрана coroutine отменяется, фоновых запросов и таймеров ядра для индикатора нет.
|
||||
|
||||
### Внутренние структуры
|
||||
|
||||
- **`struct timeout_node`** — узел таймера: name, arg, callback, expiration_ms, heap_index. Используется и для таймеров в heap, и для FIFO-очереди immediate (через поле `next`). Выделяется из `timeout_pool`.
|
||||
|
||||
@@ -212,6 +212,9 @@ struct voice_played_req {
|
||||
/* Сбор статуса (NTP + connections) и отправка в GUI. arg = struct UTUN_INSTANCE* (inst) */
|
||||
void chat_core_collect_status_trampoline(void* arg);
|
||||
|
||||
/* Лёгкий запрос нагрузки uasync для GUI. arg = inst, ответ CHAT_EVT_CPU_LOAD. */
|
||||
void chat_core_collect_cpu_load_trampoline(void* arg);
|
||||
|
||||
/* Сбор списка ETCP-соединений и отправка в GUI (binary packet). arg = inst */
|
||||
void chat_core_collect_conn_list_trampoline(void* arg);
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data,
|
||||
[45]="DM_CONVERSATIONS", [46]="ATTACHMENT_PREPARATION", [47]="DM_CALL_ROUTE",
|
||||
[55]="RADIO_MUTE_RESULT",
|
||||
[56]="MSG_SUBMITTED",
|
||||
[57]="CPU_LOAD",
|
||||
};
|
||||
const char* n = (type >= 1 && type < (int)(sizeof(names) / sizeof(names[0]))) ? names[type] : "?";
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "chat_event: %s(%d) data=%d bytes", n, type, len);
|
||||
|
||||
@@ -78,6 +78,7 @@ extern "C" {
|
||||
#define CHAT_EVT_FEEDBACK_RESULT 54 /* [request_id:8LE][success:1][JSON] — результат команды интерфейса */
|
||||
#define CHAT_EVT_RADIO_MUTE_RESULT 55 /* [group_id:8LE][node_id:8LE][success:1] — локальный переключатель рации */
|
||||
#define CHAT_EVT_MSG_SUBMITTED 56 /* [request_id:8LE][success:1] — результат локального сохранения сообщения */
|
||||
#define CHAT_EVT_CPU_LOAD 57 /* [valid:1][average_percent:1] — CPU потока uasync, среднее за секунду, 0..100 */
|
||||
|
||||
typedef void (*chat_event_handler_fn)(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
|
||||
|
||||
|
||||
@@ -330,6 +330,12 @@ static void hc_handle_status(struct headless_client* cli, int id, const char* js
|
||||
off += snprintf(buf + off, sizeof(buf) - off, "ERROR: no instance");
|
||||
} else {
|
||||
int conn_count = hc->inst->connections ? queue_entry_count(hc->inst->connections) : 0;
|
||||
struct uasync_load load;
|
||||
if (uasync_get_load(hc->ua, &load) == ERR_OK && load.valid) {
|
||||
off += snprintf(buf + off, sizeof(buf) - off, "uasync CPU: %.1f%%, average %.1f%% (window %.0f ms, age %.0f ms)\\n",
|
||||
load.current_percent, load.average_percent, load.window_us / 1000.0,
|
||||
(get_time_us() - load.sample_time_us) / 1000.0);
|
||||
} else off += snprintf(buf + off, sizeof(buf) - off, "uasync CPU: unavailable\\n");
|
||||
struct NTP_TIME* ntp = &hc->inst->ntp;
|
||||
off += snprintf(buf + off, sizeof(buf) - off, "NTP: enabled=%s synced=%s offset=%.1fs\\n",
|
||||
ntp->enabled ? "yes" : "no", ntp->synced ? "yes" : "no", ntp->offset_us / 1000000.0);
|
||||
|
||||
@@ -47,6 +47,13 @@ static void chat_core_collect_status(struct UTUN_INSTANCE* inst) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct uasync_load load;
|
||||
if (uasync_get_load(inst->ua, &load) == ERR_OK && load.valid) {
|
||||
off += snprintf(buf + off, sizeof(buf) - off, "uasync CPU: %.1f%%, average %.1f%% (window %.0f ms, age %.0f ms)\n",
|
||||
load.current_percent, load.average_percent, load.window_us / 1000.0,
|
||||
(get_time_us() - load.sample_time_us) / 1000.0);
|
||||
} else off += snprintf(buf + off, sizeof(buf) - off, "uasync CPU: unavailable\n");
|
||||
|
||||
struct NTP_TIME* ntp = &inst->ntp;
|
||||
off += snprintf(buf + off, sizeof(buf) - off, "=== NTP ===\n");
|
||||
off += snprintf(buf + off, sizeof(buf) - off, "Enabled: %s\n", ntp->enabled ? "yes" : "no");
|
||||
@@ -293,6 +300,19 @@ void chat_core_collect_status_trampoline(void* arg) {
|
||||
chat_core_collect_status(inst);
|
||||
}
|
||||
|
||||
/* Снимок снимается в потоке ядра; GUI получает только готовый процент. */
|
||||
void chat_core_collect_cpu_load_trampoline(void* arg) {
|
||||
struct UTUN_INSTANCE* inst = arg;
|
||||
if (!inst || !inst->ua) { DEBUG_WARN(DEBUG_CATEGORY_SYS, "CPU load request: core unavailable"); return; }
|
||||
struct uasync_load load;
|
||||
uint8_t data[2] = {0, 0};
|
||||
if (uasync_get_load(inst->ua, &load) == ERR_OK && load.valid) {
|
||||
data[0] = 1; data[1] = (uint8_t)(load.average_percent + 0.5);
|
||||
}
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_SYS, "CPU load GUI response: valid=%u average=%u%%", data[0], data[1]);
|
||||
chat_event_post(inst, CHAT_EVT_CPU_LOAD, data, sizeof(data));
|
||||
}
|
||||
|
||||
void chat_core_collect_conn_list_trampoline(void* arg) {
|
||||
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
|
||||
collect_conn_list(inst);
|
||||
|
||||
@@ -134,6 +134,7 @@ if OS_LINUX
|
||||
check_PROGRAMS += test_auto_socket_dynamic test_standby test_standby_transport test_group_connect_standby test_android_udp_log test_pool_corruption_log
|
||||
check_PROGRAMS += test_reality_fallback test_stcp_reality_rotation test_uasync_regressions test_ll_queue_regressions test_etcp_padding
|
||||
check_PROGRAMS += test_android_replies
|
||||
check_PROGRAMS += test_uasync_load
|
||||
endif
|
||||
|
||||
# Silero VAD: только при включённом ONNX Runtime (--with-silero-vad)
|
||||
@@ -216,6 +217,9 @@ test_etcp_padding_LDFLAGS = -Wl,--wrap=uasync_set_timeout -Wl,--wrap=socket_send
|
||||
test_uasync_regressions_SOURCES = test_uasync_regressions.c $(async_regression_sources)
|
||||
test_uasync_regressions_LDADD = $(COMMON_LIBS)
|
||||
test_uasync_regressions_LDFLAGS = $(async_regression_ldflags)
|
||||
test_uasync_load_SOURCES = test_uasync_load.c
|
||||
test_uasync_load_LDADD = $(COMMON_LIBS)
|
||||
test_uasync_load_LDFLAGS = -Wl,--wrap=clock_gettime -Wl,--wrap=epoll_wait -Wl,--wrap=poll -Wl,--wrap=epoll_create1
|
||||
test_ll_queue_regressions_SOURCES = test_ll_queue_regressions.c $(async_regression_sources)
|
||||
test_ll_queue_regressions_LDADD = $(COMMON_LIBS)
|
||||
test_ll_queue_regressions_LDFLAGS = $(async_regression_ldflags)
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/* CPU-метрика: управляемые часы/ожидание и реальная нагрузка одного потока. */
|
||||
#define _GNU_SOURCE
|
||||
#include "u_async.h"
|
||||
#include "debug_config.h"
|
||||
#include "mem.h"
|
||||
#include <errno.h>
|
||||
#include <math.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/epoll.h>
|
||||
#include <time.h>
|
||||
|
||||
#define CHECK(c) do { if (!(c)) { fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #c); exit(1); } } while (0)
|
||||
#define NEAR(a, b) CHECK(fabs((a) - (b)) < 0.0001)
|
||||
|
||||
static int simulated, clock_failure, wait_error, last_timeout, worker_stop, force_poll;
|
||||
static uint64_t wall_us, cpu_ns, wake_after_us;
|
||||
|
||||
int __real_epoll_create1(int flags);
|
||||
int __wrap_epoll_create1(int flags) {
|
||||
if (force_poll) { errno = ENOSYS; return -1; }
|
||||
return __real_epoll_create1(flags);
|
||||
}
|
||||
|
||||
int __real_clock_gettime(clockid_t id, struct timespec* ts);
|
||||
int __wrap_clock_gettime(clockid_t id, struct timespec* ts) {
|
||||
if (!simulated) return __real_clock_gettime(id, ts);
|
||||
if (id == CLOCK_THREAD_CPUTIME_ID && clock_failure) { errno = EINVAL; return -1; }
|
||||
uint64_t ns = id == CLOCK_THREAD_CPUTIME_ID ? cpu_ns : wall_us * 1000;
|
||||
ts->tv_sec = ns / 1000000000; ts->tv_nsec = ns % 1000000000;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int simulate_wait(int timeout) {
|
||||
last_timeout = timeout;
|
||||
CHECK(timeout >= 0 || wake_after_us);
|
||||
wall_us += timeout >= 0 ? (uint64_t)timeout * 1000 : wake_after_us;
|
||||
wake_after_us = 0;
|
||||
if (wait_error) { errno = wait_error; wait_error = 0; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __real_epoll_wait(int fd, struct epoll_event* events, int count, int timeout);
|
||||
int __wrap_epoll_wait(int fd, struct epoll_event* events, int count, int timeout) {
|
||||
return simulated ? simulate_wait(timeout) : __real_epoll_wait(fd, events, count, timeout);
|
||||
}
|
||||
|
||||
int __real_poll(struct pollfd* fds, nfds_t count, int timeout);
|
||||
int __wrap_poll(struct pollfd* fds, nfds_t count, int timeout) {
|
||||
return simulated ? simulate_wait(timeout) : __real_poll(fds, count, timeout);
|
||||
}
|
||||
|
||||
static struct uasync_load snapshot(struct UASYNC* ua) {
|
||||
struct uasync_load load;
|
||||
CHECK(uasync_get_load(ua, &load) == ERR_OK);
|
||||
return load;
|
||||
}
|
||||
|
||||
static struct uasync_load advance(struct UASYNC* ua, uint64_t elapsed_us, uint64_t used_cpu_us) {
|
||||
wall_us += elapsed_us; cpu_ns += used_cpu_us * 1000;
|
||||
uasync_poll(ua, 0);
|
||||
return snapshot(ua);
|
||||
}
|
||||
|
||||
static struct UASYNC* start_fake(int fallback) {
|
||||
simulated = 1; wall_us = 10000000; cpu_ns = 0; clock_failure = 0; wait_error = 0;
|
||||
force_poll = fallback;
|
||||
struct UASYNC* ua = uasync_create(); CHECK(ua);
|
||||
CHECK(!snapshot(ua).valid);
|
||||
uasync_poll(ua, 0);
|
||||
CHECK(!snapshot(ua).valid);
|
||||
return ua;
|
||||
}
|
||||
|
||||
static void delayed_callback(void* arg) {
|
||||
(void)arg;
|
||||
wall_us += 350000; cpu_ns += 70000000;
|
||||
}
|
||||
|
||||
static void deterministic(int fallback) {
|
||||
struct UASYNC* ua = start_fake(fallback);
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
uasync_poll(ua, 1000);
|
||||
struct uasync_load load = snapshot(ua);
|
||||
CHECK(last_timeout == 100); CHECK(load.valid); CHECK(load.window_us == (uint64_t)i * 100000);
|
||||
NEAR(load.current_percent, 0); NEAR(load.average_percent, 0);
|
||||
}
|
||||
// Пять нагруженных отсчётов вытесняют половину секунды простоя.
|
||||
struct uasync_load load;
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
load = advance(ua, 100000, 100000);
|
||||
NEAR(load.current_percent, 100); NEAR(load.average_percent, i * 10.0);
|
||||
CHECK(load.window_us == 1000000);
|
||||
}
|
||||
for (int i = 0; i < 5; i++) load = advance(ua, 100000, 0);
|
||||
NEAR(load.average_percent, 50);
|
||||
for (int i = 1; i <= 5; i++) { load = advance(ua, 100000, 0); NEAR(load.average_percent, 50 - i * 10.0); }
|
||||
// Нулевые проходы не создают отсчёты.
|
||||
uint64_t sampled = load.sample_time_us;
|
||||
for (int i = 0; i < 100; i++) uasync_poll(ua, 0);
|
||||
CHECK(snapshot(ua).sample_time_us == sampled);
|
||||
// EINTR на сроке измерения также публикует отсчёт.
|
||||
wait_error = EINTR; uasync_poll(ua, 1000);
|
||||
CHECK(snapshot(ua).sample_time_us == sampled + 100000);
|
||||
uasync_destroy(ua, 0);
|
||||
|
||||
ua = start_fake(fallback);
|
||||
load = advance(ua, 190000, 95000); NEAR(load.average_percent, 50);
|
||||
load = advance(ua, 10000, 0); NEAR(load.current_percent, 0); NEAR(load.average_percent, 47.5);
|
||||
for (int i = 0; i < 9; i++) load = advance(ua, 100000, 0);
|
||||
// В окне осталось 90мс первого интервала с CPU 50% = 45мс / 1с.
|
||||
CHECK(load.window_us == 1000000); NEAR(load.average_percent, 4.5);
|
||||
load = advance(ua, 100000, 0); NEAR(load.average_percent, 0);
|
||||
// Перерыв > окна: оценка по измеренному длинному интервалу, без выдуманных отсчётов.
|
||||
load = advance(ua, 2500000, 625000);
|
||||
CHECK(load.interval_us == 2500000); CHECK(load.window_us == 1000000);
|
||||
NEAR(load.current_percent, 25); NEAR(load.average_percent, 25);
|
||||
uasync_destroy(ua, 0);
|
||||
|
||||
ua = start_fake(fallback);
|
||||
CHECK(uasync_call_soon(ua, NULL, delayed_callback));
|
||||
uasync_poll(ua, 0); load = snapshot(ua);
|
||||
CHECK(last_timeout == 0); CHECK(load.interval_us == 350000); NEAR(load.current_percent, 20);
|
||||
uasync_poll(ua, 500); CHECK(last_timeout == 50); CHECK(snapshot(ua).sample_time_us == 10400000);
|
||||
clock_failure = 1; advance(ua, 100000, 0); CHECK(!snapshot(ua).valid);
|
||||
clock_failure = 0; uasync_poll(ua, 0); CHECK(!snapshot(ua).valid);
|
||||
uasync_destroy(ua, 0);
|
||||
|
||||
ua = start_fake(fallback);
|
||||
// Запросы без poll актуализируют нагрузку, частые запросы не вытесняют историю.
|
||||
for (int i = 0; i < 20000; i++) {
|
||||
wall_us += 50; cpu_ns += 25000;
|
||||
load = snapshot(ua);
|
||||
}
|
||||
NEAR(load.current_percent, 50); NEAR(load.average_percent, 50); CHECK(load.window_us == 1000000);
|
||||
// Бесконечный poll не получает таймаут от измерителя; пробуждение моделирует внешнее событие.
|
||||
wake_after_us = 2000000; uasync_poll(ua, -1); CHECK(last_timeout == -1);
|
||||
load = snapshot(ua); CHECK(load.valid); NEAR(load.current_percent, 0); NEAR(load.average_percent, 0);
|
||||
CHECK(load.window_us == 1000000);
|
||||
uasync_destroy(ua, 0);
|
||||
printf("PASS deterministic %s: cadence, window, weighting, delay, EINTR, clock failure, request, sleep\n", fallback ? "poll" : "epoll");
|
||||
}
|
||||
|
||||
static uint64_t real_time_ns(clockid_t id) {
|
||||
struct timespec ts; CHECK(__real_clock_gettime(id, &ts) == 0);
|
||||
return (uint64_t)ts.tv_sec * 1000000000 + ts.tv_nsec;
|
||||
}
|
||||
|
||||
struct workload {
|
||||
struct UASYNC* ua;
|
||||
int mixed;
|
||||
uint64_t next_us;
|
||||
void* timer;
|
||||
};
|
||||
|
||||
static void burn(void* arg) {
|
||||
struct workload* work = arg;
|
||||
work->timer = NULL;
|
||||
uint64_t end = real_time_ns(CLOCK_MONOTONIC) + (work->mixed ? 25000000 : 3000000);
|
||||
while (real_time_ns(CLOCK_MONOTONIC) < end) {}
|
||||
if (work->mixed) {
|
||||
work->next_us += 100000;
|
||||
uint64_t now = get_time_us();
|
||||
int delay_tb = now < work->next_us ? (int)((work->next_us - now + 99) / 100) : 0;
|
||||
work->timer = uasync_set_timeout(work->ua, delay_tb, work, burn, "load 25%");
|
||||
} else work->timer = uasync_call_soon(work->ua, work, burn);
|
||||
CHECK(work->timer);
|
||||
}
|
||||
|
||||
static void* other_thread(void* arg) {
|
||||
(void)arg;
|
||||
while (!__atomic_load_n(&worker_stop, __ATOMIC_ACQUIRE)) {}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void real_phase(struct UASYNC* ua, const char* name, double minimum, double maximum) {
|
||||
uint64_t start = get_time_us(), deadline = start + 1400000, last = snapshot(ua).sample_time_us;
|
||||
uint64_t before_cpu = real_time_ns(CLOCK_THREAD_CPUTIME_ID);
|
||||
unsigned int updates = 0;
|
||||
while (get_time_us() < deadline) {
|
||||
uasync_poll(ua, 1000);
|
||||
uint64_t sample = snapshot(ua).sample_time_us;
|
||||
if (sample != last) { updates++; last = sample; }
|
||||
}
|
||||
struct uasync_load load = snapshot(ua);
|
||||
double reference = (double)(real_time_ns(CLOCK_THREAD_CPUTIME_ID) - before_cpu) / (get_time_us() - start) / 10;
|
||||
printf("LIVE %-18s current=%6.2f%% average=%6.2f%% reference=%6.2f%% updates=%u window=%lluus\n",
|
||||
name, load.current_percent, load.average_percent, reference, updates, (unsigned long long)load.window_us);
|
||||
CHECK(load.valid); CHECK(load.window_us == 1000000);
|
||||
CHECK(load.average_percent >= minimum && load.average_percent <= maximum);
|
||||
CHECK(fabs(load.average_percent - reference) < 15);
|
||||
CHECK(updates >= 10 && updates <= 16);
|
||||
}
|
||||
|
||||
static void timer_fired(void* arg) { *(int*)arg = 1; }
|
||||
|
||||
static void real_load(int fallback) {
|
||||
simulated = 0;
|
||||
force_poll = fallback;
|
||||
struct UASYNC* ua = uasync_create(); CHECK(ua);
|
||||
printf("REAL %s\n", fallback ? "poll" : "epoll");
|
||||
real_phase(ua, "idle", 0, 10);
|
||||
struct workload work = {.ua = ua, .mixed = 1, .next_us = get_time_us()};
|
||||
work.timer = uasync_call_soon(ua, &work, burn); CHECK(work.timer);
|
||||
real_phase(ua, "25% wall workload", 10, 40);
|
||||
CHECK(uasync_cancel_timeout(ua, work.timer) == ERR_OK);
|
||||
work.mixed = 0; work.timer = uasync_call_soon(ua, &work, burn); CHECK(work.timer);
|
||||
real_phase(ua, "busy", 40, 100);
|
||||
CHECK(uasync_call_soon_cancel(ua, work.timer) == ERR_OK);
|
||||
real_phase(ua, "recovered idle", 0, 10);
|
||||
pthread_t worker; __atomic_store_n(&worker_stop, 0, __ATOMIC_RELEASE);
|
||||
CHECK(pthread_create(&worker, NULL, other_thread, NULL) == 0);
|
||||
real_phase(ua, "other thread busy", 0, 10);
|
||||
__atomic_store_n(&worker_stop, 1, __ATOMIC_RELEASE); CHECK(pthread_join(worker, NULL) == 0);
|
||||
// Один poll спит до прикладного таймера, без пробуждений для CPU-метрики.
|
||||
int fired = 0; CHECK(uasync_set_timeout(ua, 12000, &fired, timer_fired, "long idle"));
|
||||
uint64_t sleep_start = get_time_us();
|
||||
uasync_poll(ua, -1);
|
||||
uint64_t slept = get_time_us() - sleep_start;
|
||||
struct uasync_load load = snapshot(ua);
|
||||
CHECK(fired); CHECK(slept >= 1190000); NEAR(load.current_percent, 0); NEAR(load.average_percent, 0);
|
||||
printf("LIVE long idle current=%.2f%% average=%.2f%% one_poll=%lluus\n",
|
||||
load.current_percent, load.average_percent, (unsigned long long)slept);
|
||||
uasync_destroy(ua, 0);
|
||||
}
|
||||
|
||||
int main(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);
|
||||
size_t allocated = u_get_allocated_count();
|
||||
deterministic(0); deterministic(1);
|
||||
real_load(0); real_load(1);
|
||||
CHECK(u_get_allocated_count() == allocated);
|
||||
puts("PASS uasync CPU load");
|
||||
return 0;
|
||||
}
|
||||
@@ -358,11 +358,14 @@ class MainActivity : AppCompatActivity() {
|
||||
val channels by vm.channels.collectAsState()
|
||||
val dmConversations by vm.dmConversations.collectAsState()
|
||||
val stuck by vm.isStuck.collectAsState()
|
||||
val cpuLoad by vm.cpuLoad.collectAsState()
|
||||
ChannelListScreen(
|
||||
channels = channels,
|
||||
dmConversations = dmConversations,
|
||||
connected = connected,
|
||||
isStuck = stuck,
|
||||
cpuLoad = cpuLoad,
|
||||
onCpuLoadVisible = vm::setCpuLoadVisible,
|
||||
onSelectChannel = { ch -> vm.selectChannel(ch); screen = Screen.Chat },
|
||||
onSelectDm = { dm -> vm.openDm(dm.convId, dm.peerNodeId, dm.peerName); screen = Screen.Dm },
|
||||
onCreateChannel = { name -> vm.createChannel(name, "channel_1") },
|
||||
|
||||
@@ -84,6 +84,7 @@ object NativeLib {
|
||||
fun requestInviteLink(channelId: String, targetNodeId: Long, requestId: Long) { nativeRequestInviteLink(channelId, targetNodeId, requestId) }
|
||||
|
||||
fun ping() { nativePing() }
|
||||
fun requestCpuLoad(): Boolean = nativeRequestCpuLoad()
|
||||
fun isResponsive(): Boolean = nativeIsResponsive()
|
||||
|
||||
/* ── App foreground/background (Android lifecycle) ── */
|
||||
@@ -253,6 +254,7 @@ object NativeLib {
|
||||
private external fun nativeInit(dbPath: String, controlPort: Int, config: Any, logCb: Any): Boolean
|
||||
private external fun nativeStart(configText: String): Boolean
|
||||
private external fun nativeStop()
|
||||
private external fun nativeRequestCpuLoad(): Boolean
|
||||
private external fun nativeSetEventCallback(cb: Any)
|
||||
private external fun nativeDestroy()
|
||||
private external fun nativeSendMessage(channelId: String, text: ByteArray, replyTo: ByteArray, requestId: Long): Boolean
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.utun.chat.ui.components
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.RoundRect
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/* Градиент привязан ко всей ширине: низкая нагрузка зелёная, полная доходит до красного. */
|
||||
@Composable
|
||||
fun CpuLoadIndicator(percent: Int, modifier: Modifier = Modifier) {
|
||||
val value = percent.coerceIn(0, 100)
|
||||
Box(modifier.size(60.dp, 24.dp).semantics { contentDescription = "CPU $value%" }, contentAlignment = Alignment.Center) {
|
||||
Canvas(Modifier.matchParentSize()) {
|
||||
val shape = Path().apply { addRoundRect(RoundRect(0f, 0f, size.width, size.height, CornerRadius(5.dp.toPx()))) }
|
||||
clipPath(shape) {
|
||||
drawRect(Color(0xFFE4E9E6))
|
||||
clipRect(right = size.width * value / 100f) {
|
||||
drawRect(Brush.horizontalGradient(listOf(Color(0xFF66BB6A), Color(0xFFFFCA28), Color(0xFFEF5350)), endX = size.width))
|
||||
}
|
||||
}
|
||||
}
|
||||
Text("$value%", color = Color(0xFF172019), fontSize = 12.sp, fontWeight = FontWeight.Bold, maxLines = 1)
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import com.utun.chat.ui.components.PeerStatusDot
|
||||
import com.utun.chat.ui.components.CpuLoadIndicator
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import com.utun.chat.ui.components.GroupAvatar
|
||||
import com.utun.chat.ui.components.UserAvatar
|
||||
import com.utun.chat.ui.components.GroupPropertiesDialog
|
||||
@@ -43,6 +45,8 @@ fun ChannelListScreen(
|
||||
dmConversations: List<DmConversation> = emptyList(),
|
||||
connected: Boolean,
|
||||
isStuck: Boolean = false,
|
||||
cpuLoad: Int = 0,
|
||||
onCpuLoadVisible: (Boolean) -> Unit = {},
|
||||
onSelectChannel: (Channel) -> Unit,
|
||||
onSelectDm: (DmConversation) -> Unit = {},
|
||||
onCreateChannel: (String) -> Unit,
|
||||
@@ -53,6 +57,10 @@ fun ChannelListScreen(
|
||||
onLogs: () -> Unit,
|
||||
onRefresh: () -> Unit = {}
|
||||
) {
|
||||
LifecycleResumeEffect(Unit) {
|
||||
onCpuLoadVisible(true)
|
||||
onPauseOrDispose { onCpuLoadVisible(false) }
|
||||
}
|
||||
var showCreateDialog by remember { mutableStateOf(false) }
|
||||
var menuChannelId by remember { mutableStateOf<String?>(null) }
|
||||
var optionsChannel by remember { mutableStateOf<Channel?>(null) }
|
||||
@@ -81,6 +89,7 @@ fun ChannelListScreen(
|
||||
},
|
||||
floatingActionButton = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
CpuLoadIndicator(cpuLoad, Modifier.padding(end = 12.dp))
|
||||
Text(
|
||||
text = "v${BuildConfig.VERSION_NAME}",
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
|
||||
@@ -250,6 +250,13 @@ class ChatViewModel : ViewModel() {
|
||||
private val _isStuck = MutableStateFlow(false)
|
||||
val isStuck: StateFlow<Boolean> = _isStuck
|
||||
|
||||
private val _cpuLoad = MutableStateFlow(0)
|
||||
val cpuLoad: StateFlow<Int> = _cpuLoad
|
||||
private val _cpuLoadVisible = MutableStateFlow(false)
|
||||
private var cpuLoadPending = false
|
||||
|
||||
fun setCpuLoadVisible(visible: Boolean) { _cpuLoadVisible.value = visible }
|
||||
|
||||
private val _nodeStatuses = MutableStateFlow<Map<Long, NodeStatus>>(emptyMap())
|
||||
val nodeStatuses: StateFlow<Map<Long, NodeStatus>> = _nodeStatuses
|
||||
|
||||
@@ -324,6 +331,21 @@ class ChatViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
_cpuLoadVisible.collectLatest { visible ->
|
||||
if (!visible) return@collectLatest
|
||||
LogManager.addLog("DEBUG", "CPU", "GUI polling started: period=333ms")
|
||||
try {
|
||||
while (isActive) {
|
||||
if (!NativeLib.isReady()) { cpuLoadPending = false; _cpuLoad.value = 0 }
|
||||
else if (!cpuLoadPending) cpuLoadPending = NativeLib.requestCpuLoad()
|
||||
delay(333)
|
||||
}
|
||||
} finally {
|
||||
LogManager.addLog("DEBUG", "CPU", "GUI polling stopped")
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
AppEventHandler.events.collect { (type, data) -> handleEvent(type, data) }
|
||||
}
|
||||
@@ -386,11 +408,21 @@ class ChatViewModel : ViewModel() {
|
||||
|
||||
private fun handleEvent(type: Int, data: ByteArray?) {
|
||||
if (type == 13) {
|
||||
cpuLoadPending = false; _cpuLoad.value = 0
|
||||
dmPresenceRefreshJob?.cancel()
|
||||
_dmCallAvailable.value = false
|
||||
_dmConversations.value = _dmConversations.value.map { it.copy(online = false) }
|
||||
} else if (type in listOf(4, 5, 12, 16, 17, 24, 25, 31, 33)) refreshDmCallRoute()
|
||||
when (type) {
|
||||
57 -> { /* CPU_LOAD: valid:1, average_percent:1. */
|
||||
cpuLoadPending = false
|
||||
if (data == null || data.size != 2 || data[0].toInt() !in 0..1 || data[1].toInt() !in 0..100) {
|
||||
LogManager.addLog("ERROR", "CPU", "Invalid CPU load response: size=${data?.size}")
|
||||
_cpuLoad.value = 0
|
||||
return
|
||||
}
|
||||
if (_cpuLoadVisible.value) _cpuLoad.value = if (data[0].toInt() == 1) data[1].toInt() else 0
|
||||
}
|
||||
56 -> { /* MSG_SUBMITTED: request_id:8LE, success. */
|
||||
if (data == null || data.size != 9) { LogManager.addLog("ERROR", "Reply", "invalid submission result"); return }
|
||||
val id = java.nio.ByteBuffer.wrap(data).order(java.nio.ByteOrder.LITTLE_ENDIAN).long
|
||||
|
||||
@@ -2161,6 +2161,19 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativePing(
|
||||
utun_bridge_ping();
|
||||
}
|
||||
|
||||
/* GUI запрашивает готовый снимок асинхронно, CPU-часы читает только поток ядра. */
|
||||
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRequestCpuLoad(JNIEnv* env, jobject thiz) {
|
||||
(void)env; (void)thiz;
|
||||
struct UASYNC* ua = instance_lite_get_uasync();
|
||||
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
|
||||
if (!ua || !inst || !instance_lite_is_running()) return JNI_FALSE;
|
||||
struct posted_task* task = u_calloc(1, sizeof(*task));
|
||||
if (!task) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "CPU load request allocation failed"); return JNI_FALSE; }
|
||||
task->callback = chat_core_collect_cpu_load_trampoline; task->arg = inst;
|
||||
uasync_post_reserved(ua, task);
|
||||
return JNI_TRUE;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeConnectChannel(
|
||||
JNIEnv* env, jobject thiz, jstring channelId) {
|
||||
(void)thiz;
|
||||
|
||||
@@ -24,6 +24,38 @@ extern "C" {
|
||||
#include "dm/dm_core.h"
|
||||
}
|
||||
|
||||
/* Секундное среднее CPU: фиксированный градиент, заполнение по проценту. */
|
||||
class CpuLoadIndicator : public QWidget {
|
||||
public:
|
||||
explicit CpuLoadIndicator(QWidget* parent) : QWidget(parent) {
|
||||
setObjectName(QStringLiteral("cpuLoadIndicator"));
|
||||
setFixedSize(60, 24);
|
||||
setAccessibleName(QStringLiteral("CPU"));
|
||||
}
|
||||
void setValue(int percent) {
|
||||
percent = qBound(0, percent, 100);
|
||||
if (m_percent == percent) return;
|
||||
m_percent = percent; update();
|
||||
}
|
||||
protected:
|
||||
void paintEvent(QPaintEvent*) override {
|
||||
QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing);
|
||||
QPainterPath path; path.addRoundedRect(QRectF(rect()), 5, 5);
|
||||
painter.setClipPath(path);
|
||||
painter.fillRect(rect(), QColor("#E4E9E6"));
|
||||
QLinearGradient gradient(0, 0, width(), 0);
|
||||
gradient.setColorAt(0, QColor("#66BB6A"));
|
||||
gradient.setColorAt(0.5, QColor("#FFCA28"));
|
||||
gradient.setColorAt(1, QColor("#EF5350"));
|
||||
painter.fillRect(QRectF(0, 0, width() * m_percent / 100.0, height()), gradient);
|
||||
painter.setPen(QColor("#172019"));
|
||||
QFont f = font(); f.setBold(true); painter.setFont(f);
|
||||
painter.drawText(rect(), Qt::AlignCenter, QString::number(m_percent) + '%');
|
||||
}
|
||||
private:
|
||||
int m_percent = 0;
|
||||
};
|
||||
|
||||
/* Пустое место и заголовки секций не сбрасывают выбор канала. */
|
||||
class ChannelListView : public QListView {
|
||||
public:
|
||||
@@ -106,7 +138,12 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent)
|
||||
auto *header = new QLabel(QCoreApplication::translate("ChatGui", "Channels"), this);
|
||||
header->setAlignment(Qt::AlignCenter);
|
||||
header->setStyleSheet("font-weight: bold; padding: 8px; background: palette(window);");
|
||||
layout->addWidget(header);
|
||||
auto* headerRow = new QHBoxLayout;
|
||||
headerRow->setContentsMargins(0, 0, 8, 0);
|
||||
headerRow->addWidget(header, 1);
|
||||
m_cpuLoad = new CpuLoadIndicator(this);
|
||||
headerRow->addWidget(m_cpuLoad);
|
||||
layout->addLayout(headerRow);
|
||||
|
||||
m_listView->setModel(m_model);
|
||||
m_listView->setItemDelegate(new ChannelDelegate(this));
|
||||
@@ -266,6 +303,8 @@ void ChannelList::onDmCallRoute(const uint8_t* data, int len) {
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelList::setCpuLoad(int percent) { m_cpuLoad->setValue(percent); }
|
||||
|
||||
void ChannelList::loadChannels() {
|
||||
refreshDmPresence();
|
||||
QString selectedCid;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
class DbManager;
|
||||
class QLabel;
|
||||
class CpuLoadIndicator;
|
||||
|
||||
class ChannelList : public QWidget {
|
||||
Q_OBJECT
|
||||
@@ -25,6 +26,7 @@ public:
|
||||
void selectChannel(const QString& channelId);
|
||||
void setChannelPeersOnline(const QString& channelId, int count);
|
||||
void setAutoConnectActive(bool active);
|
||||
void setCpuLoad(int percent);
|
||||
void setChannelUnreadCount(const QString& channelId, int count);
|
||||
int getChannelUnreadCount(const QString& channelId) const;
|
||||
|
||||
@@ -49,4 +51,5 @@ private:
|
||||
bool m_autoConnectActive = false;
|
||||
QHash<QString, bool> m_dmOnline;
|
||||
QTimer* m_dmPresenceTimer;
|
||||
CpuLoadIndicator* m_cpuLoad;
|
||||
};
|
||||
@@ -171,6 +171,18 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath,
|
||||
setupConnects();
|
||||
}
|
||||
connect(m_autoPlayTimer, &QTimer::timeout, this, &MainWindow::autoPlayTimerTick);
|
||||
m_cpuLoadTimer = new QTimer(this);
|
||||
m_cpuLoadTimer->setTimerType(Qt::PreciseTimer);
|
||||
connect(m_cpuLoadTimer, &QTimer::timeout, this, [this] {
|
||||
auto* inst = gui_bridge_get_inst();
|
||||
if (!m_node || !m_node->isRunning() || !inst || !gui_bridge_is_uasync_ready()) {
|
||||
m_cpuLoadPending = false; m_channelList->setCpuLoad(0); return;
|
||||
}
|
||||
if (m_cpuLoadPending) return;
|
||||
m_cpuLoadPending = gui_bridge_request_cpu_load() != 0;
|
||||
});
|
||||
m_cpuLoadTimer->start(333);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "desktop CPU indicator polling started: period=333ms");
|
||||
|
||||
#ifndef _WIN32
|
||||
std::signal(SIGINT, unixSignalHandler);
|
||||
@@ -183,6 +195,9 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath,
|
||||
|
||||
MainWindow::~MainWindow() {
|
||||
s_mainWindow = nullptr;
|
||||
m_cpuLoadTimer->stop();
|
||||
gui_bridge_set_cpu_load_cb(nullptr);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "desktop CPU indicator polling stopped");
|
||||
if (m_messageNotification) m_messageNotification->dismiss("shutdown");
|
||||
m_messageNotification.reset();
|
||||
delete AvatarManager::instance();
|
||||
@@ -407,6 +422,11 @@ void MainWindow::setupSoundFromConfig() {
|
||||
|
||||
void MainWindow::setupBridgeCallbacks() {
|
||||
s_mainWindow = this;
|
||||
gui_bridge_set_cpu_load_cb([](int percent) {
|
||||
if (!s_mainWindow) return;
|
||||
s_mainWindow->m_cpuLoadPending = false;
|
||||
s_mainWindow->m_channelList->setCpuLoad(percent);
|
||||
});
|
||||
gui_bridge_set_member_files_response_cb([](const uint8_t* data, int length) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "member_files: GUI response route bytes=%d browser=%d",
|
||||
length, (int)(s_mainWindow && s_mainWindow->m_fileBrowser));
|
||||
@@ -554,6 +574,8 @@ void MainWindow::setupBridgeCallbacks() {
|
||||
gui_bridge_set_network_changed_cb([](int type) {
|
||||
if (!s_mainWindow) return;
|
||||
if (type == 13) {
|
||||
s_mainWindow->m_cpuLoadPending = false;
|
||||
s_mainWindow->m_channelList->setCpuLoad(0);
|
||||
s_mainWindow->m_accountList->model()->setRadioState(0, false, false);
|
||||
s_mainWindow->m_messageList->clearDmCallRoute();
|
||||
s_mainWindow->m_channelList->clearDmPresence();
|
||||
|
||||
@@ -129,6 +129,8 @@ private:
|
||||
|
||||
QQueue<AutoPlayItem> m_autoPlayQueue;
|
||||
QTimer* m_autoPlayTimer = nullptr;
|
||||
QTimer* m_cpuLoadTimer = nullptr;
|
||||
bool m_cpuLoadPending = false;
|
||||
QTimer* m_shutdownTimer = nullptr;
|
||||
QSet<int64_t> m_autoPlayDownloading; /* msgIds with download in progress */
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/* Формат канального события: live/history сохраняются при передаче в GUI, битые события отбрасываются. */
|
||||
#include "../transport/gui_bridge.h"
|
||||
#include "../../../lib/debug_config.h"
|
||||
#include "../../../src/chat/chat_event.h"
|
||||
#include <QtTest>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
|
||||
struct ReceivedMessage {
|
||||
QString channel;
|
||||
@@ -11,6 +13,13 @@ struct ReceivedMessage {
|
||||
bool guiThread;
|
||||
};
|
||||
static QList<ReceivedMessage> received;
|
||||
static QList<int> cpuLoads;
|
||||
static bool cpuOnGuiThread = true;
|
||||
|
||||
static void cpu_received(int percent) {
|
||||
cpuLoads.append(percent);
|
||||
cpuOnGuiThread &= QThread::currentThread() == qApp->thread();
|
||||
}
|
||||
|
||||
static void message_received(const char* channel, int length, uint64_t author, int initial_sync) {
|
||||
received.append({QString::fromUtf8(channel, length), author, initial_sync != 0, QThread::currentThread() == qApp->thread()});
|
||||
@@ -39,7 +48,7 @@ private slots:
|
||||
gui_bridge_init();
|
||||
gui_bridge_set_msg_received_cb(message_received);
|
||||
}
|
||||
void cleanupTestCase() { gui_bridge_set_msg_received_cb(nullptr); }
|
||||
void cleanupTestCase() { gui_bridge_set_msg_received_cb(nullptr); gui_bridge_set_cpu_load_cb(nullptr); }
|
||||
void init() { received.clear(); }
|
||||
|
||||
void liveAndHistory() {
|
||||
@@ -69,6 +78,26 @@ private slots:
|
||||
QTest::qWait(100);
|
||||
QVERIFY(received.isEmpty());
|
||||
}
|
||||
|
||||
void cpuLoadDeliveredToGuiThread() {
|
||||
cpuLoads.clear(); cpuOnGuiThread = true;
|
||||
gui_bridge_set_cpu_load_cb(cpu_received);
|
||||
std::thread worker([] {
|
||||
const uint8_t samples[][2] = {{1, 0}, {1, 50}, {1, 100}, {0, 90}, {1, 101}};
|
||||
for (const auto& sample : samples) gui_bridge_post(CHAT_EVT_CPU_LOAD, sample, sizeof(sample));
|
||||
gui_bridge_post(CHAT_EVT_CPU_LOAD, nullptr, 0);
|
||||
});
|
||||
worker.join();
|
||||
QVERIFY(cpuLoads.isEmpty()); // Worker не вызывает UI напрямую.
|
||||
QTRY_COMPARE(cpuLoads.size(), 6);
|
||||
QCOMPARE(cpuLoads, QList<int>({0, 50, 100, 0, 0, 0}));
|
||||
QVERIFY(cpuOnGuiThread);
|
||||
gui_bridge_set_cpu_load_cb(nullptr);
|
||||
const uint8_t sample[] = {1, 75};
|
||||
gui_bridge_post(CHAT_EVT_CPU_LOAD, sample, sizeof(sample));
|
||||
QCoreApplication::processEvents();
|
||||
QCOMPARE(cpuLoads.size(), 6);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(MessageEventTest)
|
||||
|
||||
@@ -121,6 +121,11 @@ void gui_bridge_set_db_ready_cb(gui_db_ready_fn cb);
|
||||
typedef void (*gui_status_refresh_fn)(const char* text, int len);
|
||||
void gui_bridge_set_status_refresh_cb(gui_status_refresh_fn cb);
|
||||
|
||||
/* Секундное среднее CPU uasync, 0..100; callback выполняется в GUI-потоке. */
|
||||
typedef void (*gui_cpu_load_fn)(int percent);
|
||||
void gui_bridge_set_cpu_load_cb(gui_cpu_load_fn cb);
|
||||
int gui_bridge_request_cpu_load(void); // 1 = запрос поставлен в очередь; 0 = ядро недоступно/ошибка
|
||||
|
||||
/* Callback для завершения загрузки медиа-аттача */
|
||||
typedef void (*gui_attachment_downloaded_fn)(const char* ch_id, int ch_id_len, int64_t msg_id);
|
||||
void gui_bridge_set_attachment_downloaded_cb(gui_attachment_downloaded_fn cb);
|
||||
|
||||
@@ -14,6 +14,7 @@ extern "C" {
|
||||
#include "../../../src/routing_layer/topo_group.h"
|
||||
#include "../../../src/member_files/protocol.h"
|
||||
#include "../../../src/chat/chat_member.h"
|
||||
#include "../../../src/chat/chat_core.h"
|
||||
}
|
||||
|
||||
/* ── Внутренний объект-приёмник в GUI-потоке ── */
|
||||
@@ -37,6 +38,7 @@ static gui_auto_connect_status_fn g_auto_connect_status_cb = nullptr;
|
||||
static gui_channel_peers_online_fn g_channel_peers_online_cb = nullptr;
|
||||
static gui_db_ready_fn g_db_ready_cb = nullptr;
|
||||
static gui_status_refresh_fn g_status_refresh_cb = nullptr;
|
||||
static gui_cpu_load_fn g_cpu_load_cb = nullptr;
|
||||
static gui_attachment_downloaded_fn g_attachment_downloaded_cb = nullptr;
|
||||
static gui_download_progress_fn g_download_progress_cb = nullptr;
|
||||
static gui_nodeinfo_update_fn g_nodeinfo_update_cb = nullptr;
|
||||
@@ -89,6 +91,12 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) {
|
||||
eventType == 16 || eventType == 17 || eventType == 24 || eventType == 25 || eventType == 31 || eventType == 33))
|
||||
g_network_changed_cb(eventType);
|
||||
switch (eventType) {
|
||||
case CHAT_EVT_CPU_LOAD:
|
||||
if (dlen != 2 || d[0] > 1 || d[1] > 100) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "invalid CPU load response bytes=%d", dlen);
|
||||
if (g_cpu_load_cb) g_cpu_load_cb(0);
|
||||
} else if (g_cpu_load_cb) g_cpu_load_cb(d[0] ? d[1] : 0);
|
||||
break;
|
||||
case CHAT_EVT_MSG_SUBMITTED:
|
||||
if (dlen != 9 || d[8] > 1) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "invalid submission result bytes=%d", dlen); break; }
|
||||
if (g_msg_submitted_cb) g_msg_submitted_cb(d, dlen);
|
||||
@@ -497,6 +505,19 @@ void gui_bridge_set_status_refresh_cb(gui_status_refresh_fn cb) {
|
||||
g_status_refresh_cb = cb;
|
||||
}
|
||||
|
||||
void gui_bridge_set_cpu_load_cb(gui_cpu_load_fn cb) {
|
||||
g_cpu_load_cb = cb;
|
||||
}
|
||||
|
||||
int gui_bridge_request_cpu_load(void) {
|
||||
if (!g_ua || !g_inst) return 0;
|
||||
auto* task = (struct posted_task*)u_calloc(1, sizeof(struct posted_task));
|
||||
if (!task) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "CPU load request allocation failed"); return 0; }
|
||||
task->callback = chat_core_collect_cpu_load_trampoline; task->arg = g_inst;
|
||||
uasync_post_reserved(g_ua, task);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void gui_bridge_set_attachment_downloaded_cb(gui_attachment_downloaded_fn cb) {
|
||||
g_attachment_downloaded_cb = cb;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user