Analyse de la charge serveur

Version du document 29/05/2026 · Contexte : 1300 ble_token_renew d’un seul invité pendant 4 heures d’absence du propriétaire en ligne, ce qui a lancé la recherche d’autres sources de trafic HTTP et WebSocket superflu.

Déjà corrigé en 6.61–6.63

n° 1 — reprises HTTP sans attente 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 prend TOUTES les clés hôte en attente et appelle Api.keyCreatepour chacune. Si l’appel HTTP échoue, la clé reste en attente et 60 s plus tard le cycle recommence.

Le scénario d’emballement

Correctif : attente exponentielle par clé

В Prefs stocker par localId :

À chaque passage 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 (plafond). En cas d’échec permanent, cela fait une requête toutes les 30 minutes au lieu d’une par minute. Une réduction par trente.

Il en va de même pour Vault.retryPendingBundles() — le même schéma.

n° 2 — les instantanés du flux ne sont PAS notre charge SKIP

snap_url est l’adresse externe d’une caméra, d’un NVR ou d’une source HTTP. Le client s’y rend directement via Coil ou ExoPlayer ; notre serveur ne fait que transmettre le data_cipher chiffré contenant l’adresse dans le lot. Aucune image ne passe par nous.

Si un propriétaire règle snap_int=1 pour 10 invités, son NVR reçoit 10 requêtes par seconde. C’est une charge pour son NVR, pas pour notre serveur.

Facultatif : borner le curseur de l’interface à trois secondes. C’est une question d’interface, pas d’infrastructure.

n° 3 — le planificateur ble_renew SKIP

Déjà bridé par les correctifs de la 6.61 :

Avec dix objets BLE, un utilisateur envoie dix messages WebSocket d’affilée et le serveur exécute dix branches case 'ble_token_renew' à la suite. Une bagatelle.

n° 4 — l’incrément d’overridesVersion FIX

File: GuestConn.kt — 7+ endroits.

AppState.overridesVersion = MutableState<Int>, un signal Compose signifiant les données ont changé, relis remember . Il est utilisé dans 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

Chaque incrément force chaque carte à relire la substitution, l’avatar, l’instantané et le modèle depuis SharedPreferences. Un utilisateur avec 20 cartes, cela fait 80 lectures de SharedPreferences par incrément.

Où se trouvent les incréments

LineContextNécessaire ?
L193après réingestion d’un lot (pas de K_obj)yes
L355après parseNumbers dans numbers_updateredundant quand les numéros n’ont pas changé
L411après le chargement d’un avataryes
L558, L595après parseNumbers dans les autres gestionnairesredundant quand les données sont identiques

Où cela charge indirectement le serveur

Avec un WebSocket instable, chaque guest_ok → le gestionnaire réanalyse les numéros → incrémente overridesVersion → 80 lectures de SharedPreferences × 20 cartes × la fréquence de guest_ok.

C’est du processeur local et des E/S, pas une charge directe du serveur, mais :

Correctif : comparer avant d’incrémenter

Avant d’incrémenter, comparer la nouvelle liste JSON de numbers avec la précédente, par hachage ou en comparant la liste des GuestNumberInfo). Identiques → ne pas incrémenter.

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 arrive régulièrement mais change rarement — un propriétaire n’édite pas ses objets toutes les minutes. L’optimisation supprime environ 90 % des vagues de redessin superflues.

Conclusion : quoi corriger

n° 1 (reprises HTTP sans attente) — charge HTTP directe. Une simple attente par clé élimine 97 % des requêtes sur les réseaux difficiles.
n° 4 (l’incrément d’overridesVersion) — indirecte, par la consommation de batterie et de processeur. Comparer avant d’incrémenter supprime 90 % des vagues de recomposition.
n° 2 et n° 3 sont hors sujet : le flux vient d’un NVR ou d’un CDN externe, sans passer par nous, et le planificateur de renouvellements est déjà bridé par les correctifs de la 6.61.