Análisis de la carga del servidor

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.

Ya corregido en la 6.61–6.63

n.º 1: reintentos HTTP sin espera 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 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.

El escenario desbocado

Corrección: espera exponencial por clave

В Prefs guardar por localId:

En 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.

n.º 2: las capturas del vídeo NO son carga nuestra SKIP

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.

n.º 3: el planificador ble_renew SKIP

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.

n.º 4: incrementar overridesVersion FIX

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.

Dónde están los incrementos

LineContext¿Hace falta?
L193tras reingerir un paquete (sin K_obj)yes
L355tras parseNumbers en numbers_updateredundant cuando los números no cambiaron
L411tras cargar un avataryes
L558, L595tras parseNumbers en los demás manejadoresredundant cuando los datos son idénticos

Dónde carga esto al servidor de forma indirecta

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:

Corrección: comparar antes de incrementar

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.

Conclusión: qué corregir

n.º 1 (reintentos HTTP sin espera) — carga HTTP directa. Una simple espera por clave recorta el 97 % de las peticiones en redes problemáticas.
n.º 4 (incrementar overridesVersion) — indirecta, por el gasto de batería y CPU. Comparar antes de incrementar elimina el 90 % de las oleadas de recomposición.
n.º 2 y n.º 3 quedan fuera del orden del día: el vídeo viene de un NVR o una CDN externos, no por nosotros, y el planificador de renovaciones ya está domado por las correcciones de la 6.61.