Versión del documento 2026-05-29 · Contexto: 1300 ble_token_renew de un solo invitado durante 4 horas con el propietario desconectado, lo que impulsó la búsqueda de otras fuentes de tráfico HTTP y WebSocket sobrante.
ble_token_renew in pending_host_msgs (un UNIQUE INDEX sobre host_id, dedup_key). La migración está aplicada.GuestConn.requestBleTokenRenew — no más de una vez cada 60 s por bleId.onOpen; espera una sesión estable de al menos 60 s, lo que protege de la tormenta conectar–caer–conectar.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 toma TODAS las claves de anfitrión pendientes y llama a Api.keyCreatepor cada una. Si la llamada HTTP falla, la clave sigue pendiente y 60 s después el ciclo se repite.
В Prefs guardar por localId:
attempt_at_<localId> — la marca de tiempo del último intentoattempt_count_<localId> — el contador de intentosEn cada pasada de 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 s → 1 min → 2 min → 4 min → 8 min → 16 min → 30 min (tope). Con un fallo permanente eso es una petición cada 30 minutos en vez de una por minuto. Una reducción de treinta veces.
Lo mismo vale para Vault.retryPendingBundles() — el mismo patrón.
snap_url es la dirección externa de una cámara, un NVR o una fuente HTTP. El cliente va a esa URL directamente con Coil o ExoPlayer; nuestro servidor solo pasa el data_cipher cifrado que contiene la dirección dentro del paquete. Por nosotros no pasa ni un fotograma.
Si un propietario pone snap_int=1 para 10 invitados, su NVR recibe 10 peticiones por segundo. Esa es carga de su NVR, no de nuestro servidor.
Opcional: limitar por abajo el deslizador de la interfaz a tres segundos. Es cuestión de interfaz, no de infraestructura.
Ya está domado por las correcciones de la 6.61:
Con diez objetos BLE, un usuario envía diez mensajes WebSocket seguidos y el servidor ejecuta diez ramas case 'ble_token_renew' seguidas. Una nadería.
File: GuestConn.kt — 7+ lugares.
AppState.overridesVersion = MutableState<Int>, una señal de Compose que significa los datos cambiaron, vuelve a leer remember. Se usa en 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
Cada incremento hace que cada tarjeta relea la anulación, el avatar, la captura y la plantilla de SharedPreferences. Un usuario con 20 tarjetas significa 80 lecturas de SharedPreferences por incremento.
| Line | Context | ¿Hace falta? |
|---|---|---|
| L193 | tras reingerir un paquete (sin K_obj) | yes |
| L355 | tras parseNumbers en numbers_update | redundant cuando los números no cambiaron |
| L411 | tras cargar un avatar | yes |
| L558, L595 | tras parseNumbers en los demás manejadores | redundant cuando los datos son idénticos |
Con un WebSocket inestable, cada guest_ok → el manejador vuelve a analizar los números → incrementa overridesVersion → 80 lecturas de SharedPreferences × 20 tarjetas × la frecuencia de guest_ok.
Eso es CPU local y E/S, no carga directa del servidor, pero:
Antes de incrementar, comparar la nueva lista JSON de numbers con la anterior, por hash o comparando la lista de GuestNumberInfo). Idénticos → no incrementar.
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 llega con regularidad pero rara vez cambia de verdad: un propietario no edita objetos cada minuto. La optimización elimina cerca del 90 % de las oleadas de redibujado sobrantes.