Анализ нагрузки на сервер

Версия документа: 2026-05-29 · Контекст: 1300 ble_token_renew от одного гостя за 4 часа простоя хозяина → исследование других источников избыточных HTTP/WS-запросов.

Уже исправлено в 6.61–6.63

#1 — HTTP retry без backoff FIX

Файлы: HostService.kt:1424-1432, Vault.kt:213

private val pendingKeysRunnable = object : Runnable {
    override fun run() {
        if (Prefs.isHostRegistered && Prefs.getPendingHostKeys().isNotEmpty()) {
            Thread { syncPendingHostKeys() }.start()
        }
        pendingKeysHandler.postDelayed(this, 60_000L)
    }
}

syncPendingHostKeys берёт ВСЕ pending host-ключи и для каждого дёргает Api.keyCreate. Если HTTP падает — ключ остаётся в pending. Через 60с цикл повторяется.

Сценарий жора

Решение: per-key экспоненциальный backoff

В Prefs хранить per-localId:

При запуске syncPendingHostKeys:

val waitMs = (30_000L * (1L shl attemptCount)).coerceAtMost(30 * 60_000L)
if (now - attemptAt < waitMs) return@forEach  // скип
// иначе пробуем
val ok = try { Api.keyCreate(...); true } catch (e) { false }
if (ok) { clear attempt_at/count } else { attempt_count++; attempt_at = now }

Прогрессия: 30с → 1м → 2м → 4м → 8м → 16м → 30м (cap). При перманентной проблеме — 1 запрос в 30 минут вместо 1/мин. Снижение в 30 раз.

Аналогично для Vault.retryPendingBundles() — тот же паттерн.

#2 — Stream snapshot НЕ наша нагрузка SKIP

snap_url = внешний адрес камеры/NVR/HTTP-источника. Клиент через Coil/ExoPlayer идёт по URL напрямую, наш сервер только передаёт зашифрованный data_cipher с этим URL в bundle. Кадры через нас не идут.

Если владелец поставил snap_int=1 для 10 гостей — у него на NVR будет 10 RPS. Его проблема и его NVR.

Опционально: поставить min cap = 3с в UI слайдере. Это UX, не инфра.

#3 — ble_renew scheduler SKIP

После 6.61 фиксов уже зарегулировано:

При 10 BLE-объектов у юзера улетают 10 WS-сообщений подряд. На сервере 10 секций case 'ble_token_renew' подряд. Тривиально.

#4 — overridesVersion бамп FIX

Файл: GuestConn.kt — 7+ мест.

AppState.overridesVersion = MutableState<Int>, сигнал Compose «данные изменились, перечитай remember». Используется в ActionCard:

val ovrVersion = AppState.overridesVersion.value
val (overrideLabel, avatarPath) = remember(action.key, ovrVersion) {
    Prefs.getOverride(action.key)   // SharedPreferences IO
}
val snapCfg = remember(action.key, ovrVersion) {
    if (action.source == ActionSource.HOST_OWN) Prefs.getSnapshot(action.numberId)
    else Prefs.getIncomingSnapshot(...)
}
val tpl = remember(action.key, ovrVersion) { loadTpl() }  // ещё IO

Каждый бамп = каждая карточка перечитывает override/аватар/snapshot/template из SharedPreferences. Юзер с 20 карточками = 80 SP-чтений за бамп.

Места бампа

СтрокаКонтекстНужен?
L193после re-ingest bundle (отсутствует K_obj)да
L355после parseNumbers в numbers_updateизбыточно если numbers не изменились
L411после загрузки аватарада
L558, L595после parseNumbers в других хендлерахизбыточно при идентичных данных

Где косвенно жрёт сервер

При флапающем WS каждый guest_ok → хендлер пере-парсит numbers → бампает overridesVersion → 80 SP-чтений × 20 карточек × частота guest_ok.

Это локальный CPU + I/O, не прямая нагрузка на сервер, но:

Решение: compare-before-bump

Перед бампом сравнить новый JSON-список numbers с предыдущим (hash или прямое сравнение списка GuestNumberInfo). Идентично → не бампать.

private var lastNumbersHash: Int = 0

private fun maybeUpdateNumbers(arr: JSONArray?) {
    val parsed = parseNumbers(arr)
    val newHash = parsed.hashCode()  // или JSON-сериализация хэш
    if (newHash == lastNumbersHash) return  // ничего не изменилось
    lastNumbersHash = newHash
    numbers = parsed
    publish()
    AppState.overridesVersion.value = AppState.overridesVersion.value + 1
}

numbers_update приходит периодически, но реально меняется редко (хозяин не каждую минуту редактирует объекты). Оптимизация даст ~90% recompose-волн впустую.

Итог: что чинить

#1 (HTTP retry без backoff) — прямая HTTP-нагрузка. Простой per-key backoff даёт −97% запросов в проблемных сетях.
#4 (overridesVersion бамп) — непрямая через жор батареи и CPU. Compare-before-bump убирает 90% recompose-волн.
#2, #3 — снимаем с повестки. Стрим = внешний NVR/CDN, не наш. Renew scheduler уже зарегулирован 6.61-фиксами.

Текст подготовлен для удобства чтения вне чата. Когда подтвердишь — иду делать #1 и #4.