Analyse der Serverlast

Dokumentversion 29.05.2026 · Kontext: 1300 ble_token_renew von einem einzigen Gast in 4 Stunden Offline-Zeit des Besitzers — das gab den Anstoß, weitere Quellen überflüssigen HTTP- und WebSocket-Verkehrs zu suchen.

Bereits in 6.61–6.63 behoben

#1 — HTTP-Wiederholungen ohne Backoff 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 nimmt ALLE ausstehenden Host-Schlüssel und ruft Api.keyCreatefür jeden. Scheitert der HTTP-Aufruf, bleibt der Schlüssel ausstehend, und 60 s später wiederholt sich der Zyklus.

Das Ausreißer-Szenario

Behebung: exponentieller Backoff je Schlüssel

В Prefs je localId speichern:

Bei jedem Durchlauf von 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 (Deckel). Bei dauerhaftem Fehler ist das eine Anfrage alle 30 Minuten statt einer pro Minute. Eine dreißigfache Senkung.

Dasselbe gilt für Vault.retryPendingBundles() — dasselbe Muster.

#2 — Kamera-Schnappschüsse sind NICHT unsere Last SKIP

snap_url ist die externe Adresse einer Kamera, eines NVR oder einer HTTP-Quelle. Der Client geht direkt über Coil oder ExoPlayer dorthin; unser Server reicht nur den verschlüsselten data_cipher , der die Adresse im Bundle enthält, weiter. Keine Bilder laufen über uns.

Setzt ein Besitzer snap_int=1 für 10 Gäste, sieht sein NVR 10 Anfragen pro Sekunde. Das ist Last auf seinem NVR, nicht auf unserem Server.

Optional: den Schieberegler in der Oberfläche nach unten auf drei Sekunden begrenzen. Das ist eine Frage der Oberfläche, nicht der Infrastruktur.

#3 — der ble_renew-Scheduler SKIP

Bereits durch die Korrekturen von 6.61 gezügelt:

Bei zehn BLE-Objekten schickt ein Nutzer zehn WebSocket-Nachrichten nacheinander, und der Server durchläuft zehn case 'ble_token_renew' -Zweige nacheinander. Eine Kleinigkeit.

#4 — das Hochzählen von overridesVersion FIX

File: GuestConn.kt — 7+ Stellen.

AppState.overridesVersion = MutableState<Int>, ein Compose-Signal mit der Bedeutung „die Daten haben sich geändert, lies remember“. Verwendet wird es in 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

Jedes Hochzählen zwingt jede Karte, Override, Avatar, Schnappschuss und Vorlage neu zu lesen aus SharedPreferences. Ein Nutzer mit 20 Karten bedeutet 80 SharedPreferences-Lesevorgänge je Hochzählen.

Wo die Hochzählungen sitzen

LineContextNötig?
L193nach erneutem Einlesen eines Bundles (kein K_obj)yes
L355nach parseNumbers in numbers_updateredundant wenn sich die Nummern nicht geändert haben
L411nach dem Laden eines Avatarsyes
L558, L595nach parseNumbers in den übrigen Handlernredundant wenn die Daten identisch sind

Wo das den Server indirekt belastet

Bei flatterndem WebSocket führt jedes guest_ok → der Handler parst die Nummern neu → zählt overridesVersion hoch → 80 SharedPreferences-Lesevorgänge × 20 Karten × die guest_ok-Rate.

Das ist lokale CPU und I/O, keine direkte Last auf dem Server, aber:

Behebung: vor dem Hochzählen vergleichen

Vor dem Hochzählen die neue JSON-Liste der numbers mit der vorherigen vergleichen — per Hash oder durch Vergleich der Liste von GuestNumberInfo). Identisch → nicht hochzählen.

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 kommt regelmäßig, ändert sich aber selten — ein Besitzer bearbeitet Objekte nicht jede Minute. Die Optimierung entfernt rund 90 % der überflüssigen Neuzeichenwellen.

Fazit: was zu beheben ist

#1 (HTTP-Wiederholungen ohne Backoff) — direkte HTTP-Last. Ein einfacher Backoff je Schlüssel schneidet in schwierigen Netzen 97 % der Anfragen weg.
#4 (das Hochzählen von overridesVersion) — indirekt, über Akku- und CPU-Verbrauch. Der Vergleich vor dem Hochzählen entfernt 90 % der Recompose-Wellen.
#2 und #3 sind vom Tisch: Der Strom kommt von einem externen NVR oder CDN, an uns vorbei, und der Verlängerungs-Scheduler ist durch die Korrekturen von 6.61 bereits gezügelt.