Версия документа 2026-05-29 · Контекст: 1300 ble_token_renew от одного гостя за 4 часа отсутствия владельца в сети — это и подтолкнуло к поиску других источников лишнего трафика HTTP и WebSocket.
ble_token_renew in pending_host_msgs (UNIQUE INDEX по host_id, dedup_key). Миграция применена.GuestConn.requestBleTokenRenew — не чаще раза в 60 с на bleId.onOpen; он ждёт устойчивой сессии хотя бы в 60 с, что защищает от шторма подключился — отвалился — подключился.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:
attempt_at_<localId> — отметка времени последней попыткиattempt_count_<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() — тот же приём.
snap_url — внешний адрес камеры, регистратора или HTTP-источника. Клиент ходит по нему напрямую через Coil или ExoPlayer, а наш сервер лишь передаёт зашифрованный data_cipher , в котором адрес лежит внутри бандла. Кадры через нас не идут.
Если владелец выставит snap_int=1 для 10 гостей, его регистратор получит 10 запросов в секунду. Это нагрузка на его регистратор, а не на наш сервер.
По желанию: ограничить ползунок в интерфейсе снизу тремя секундами. Это вопрос интерфейса, а не инфраструктуры.
Уже обуздан исправлениями 6.61:
С десятью BLE-объектами пользователь шлёт десять сообщений WebSocket подряд, а сервер подряд выполняет десять веток case 'ble_token_renew' подряд. Пустяк.
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 на инкремент.
| Line | Context | Нужен? |
|---|---|---|
| L193 | после повторного приёма бандла (нет K_obj) | yes |
| L355 | после parseNumbers в numbers_update | redundant когда номера не менялись |
| 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% лишних волн перерисовки.