Разбор нагрузки на сервер

Версия документа 2026-05-29 · Контекст: 1300 ble_token_renew от одного гостя за 4 часа отсутствия владельца в сети — это и подтолкнуло к поиску других источников лишнего трафика HTTP и WebSocket.

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

№1 — повторы HTTP без отката FIX

Files: 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 берёт ВСЕ ожидающие владельческие ключи и вызывает Api.keyCreateдля каждого. Если вызов HTTP не удался, ключ остаётся в ожидании, а через 60 с цикл повторяется.

Сценарий разгона

Исправление: экспоненциальный откат на каждый ключ

В Prefs хранить по localId:

На каждом проходе syncPendingHostKeys:

val waitMs = (30_000L * (1L shl attemptCount)).coerceAtMost(30 * 60_000L)
if (now - attemptAt < waitMs) return@forEach  // skip
// otherwise try
val ok = try { Api.keyCreate(...); true } catch (e) { false }
if (ok) { clear attempt_at/count } else { attempt_count++; attempt_at = now }

Progression: 30 с → 1 мин → 2 мин → 4 мин → 8 мин → 16 мин → 30 мин (потолок). При постоянном сбое это один запрос в 30 минут вместо одного в минуту. Снижение втридцатеро.

То же относится к Vault.retryPendingBundles() — тот же приём.

№2 — снимки с камеры это НЕ наша нагрузка SKIP

snap_url — внешний адрес камеры, регистратора или HTTP-источника. Клиент ходит по нему напрямую через Coil или ExoPlayer, а наш сервер лишь передаёт зашифрованный data_cipher , в котором адрес лежит внутри бандла. Кадры через нас не идут.

Если владелец выставит snap_int=1 для 10 гостей, его регистратор получит 10 запросов в секунду. Это нагрузка на его регистратор, а не на наш сервер.

По желанию: ограничить ползунок в интерфейсе снизу тремя секундами. Это вопрос интерфейса, а не инфраструктуры.

№3 — планировщик ble_renew SKIP

Уже обуздан исправлениями 6.61:

С десятью BLE-объектами пользователь шлёт десять сообщений WebSocket подряд, а сервер подряд выполняет десять веток case 'ble_token_renew' подряд. Пустяк.

№4 — инкремент overridesVersion FIX

File: 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() }  // more IO

Каждый инкремент заставляет каждую карточку перечитать переопределение, аватар, снимок и шаблон из SharedPreferences. Пользователь с 20 карточками — это 80 чтений SharedPreferences на инкремент.

Где стоят инкременты

LineContextНужен?
L193после повторного приёма бандла (нет K_obj)yes
L355после parseNumbers в numbers_updateredundant когда номера не менялись
L411после загрузки аватараyes
L558, L595после parseNumbers в остальных обработчикахredundant когда данные идентичны

Где это косвенно нагружает сервер

При нестабильном WebSocket каждый guest_ok → обработчик заново разбирает номера → инкрементирует overridesVersion → 80 чтений SharedPreferences × 20 карточек × частота guest_ok.

Это локальный процессор и ввод-вывод, а не прямая нагрузка на сервер, но:

Исправление: сверять перед инкрементом

Перед инкрементом сравнить новый JSON-список numbers с предыдущим — по хешу или сравнив список GuestNumberInfo). Совпали → не инкрементировать.

private var lastNumbersHash: Int = 0

private fun maybeUpdateNumbers(arr: JSONArray?) {
    val parsed = parseNumbers(arr)
    val newHash = parsed.hashCode()  // or a hash of the JSON serialisation
    if (newHash == lastNumbersHash) return  // nothing changed
    lastNumbersHash = newHash
    numbers = parsed
    publish()
    AppState.overridesVersion.value = AppState.overridesVersion.value + 1
}

numbers_update приходит регулярно, но меняется редко — владелец не правит объекты каждую минуту. Оптимизация убирает около 90% лишних волн перерисовки.

Вывод: что исправлять

№1 (повторы HTTP без отката) — прямая нагрузка HTTP. Простой откат на каждый ключ срезает 97% запросов на плохих сетях.
№4 (инкремент overridesVersion) — косвенная, через расход батареи и процессора. Сверка перед инкрементом убирает 90% волн перерисовки.
№2 и №3 сняты с повестки: поток идёт с внешнего регистратора или CDN, мимо нас, а планировщик продлений уже обуздан исправлениями 6.61.