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.
ble_token_renew in pending_host_msgs (ein UNIQUE INDEX auf host_id, dedup_key). Die Migration ist angewandt.GuestConn.requestBleTokenRenew — höchstens alle 60 s je bleId.onOpen; er wartet auf eine stabile Sitzung von mindestens 60 s, was vor einem Verbinden-Abbrechen-Verbinden-Sturm schützt.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.
В Prefs je localId speichern:
attempt_at_<localId> — der Zeitstempel des letzten Versuchsattempt_count_<localId> — der VersuchszählerBei 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.
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.
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.
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.
| Line | Context | Nötig? |
|---|---|---|
| L193 | nach erneutem Einlesen eines Bundles (kein K_obj) | yes |
| L355 | nach parseNumbers in numbers_update | redundant wenn sich die Nummern nicht geändert haben |
| L411 | nach dem Laden eines Avatars | yes |
| L558, L595 | nach parseNumbers in den übrigen Handlern | redundant wenn die Daten identisch sind |
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:
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.