Версия документа: 2026-05-29 · Контекст: 1300 ble_token_renew от одного гостя за 4 часа простоя хозяина → исследование других источников избыточных HTTP/WS-запросов.
ble_token_renew в pending_host_msgs (UNIQUE INDEX по host_id, dedup_key). Миграция применена.GuestConn.requestBleTokenRenew — не чаще раза в 60с на bleId.onOpen, ждёт стабильную сессию ≥60с (защита от шторма «коннект-разрыв-коннект»).Файлы: 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с цикл повторяется.
В Prefs хранить per-localId:
attempt_at_<localId> — timestamp последней попыткиattempt_count_<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() — тот же паттерн.
snap_url = внешний адрес камеры/NVR/HTTP-источника. Клиент через Coil/ExoPlayer идёт по URL напрямую, наш сервер только передаёт зашифрованный data_cipher с этим URL в bundle. Кадры через нас не идут.
Если владелец поставил snap_int=1 для 10 гостей — у него на NVR будет 10 RPS. Его проблема и его NVR.
Опционально: поставить min cap = 3с в UI слайдере. Это UX, не инфра.
После 6.61 фиксов уже зарегулировано:
При 10 BLE-объектов у юзера улетают 10 WS-сообщений подряд. На сервере 10 секций case 'ble_token_renew' подряд. Тривиально.
Файл: 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, не прямая нагрузка на сервер, но:
Перед бампом сравнить новый 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 и #4.