From 1ab593b82705f2502d6d14d0c6a47f09e21087e4 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Tue, 18 Aug 2026 08:21:23 -0300 Subject: [PATCH] Debounce relay stat counter updates to avoid notification rate limit AmberRelayStats.addSent/addFailed called notify(id=2) synchronously for every relay event, driven by NostrClientLoggerListener.onSent. Bursts of relay traffic (7-34 notifies/sec vs Android's ~5/sec per-app limit) made NotificationManagerService shed the updates, freezing the status notification on stale text, and wasted a full BigTextStyle build plus Binder IPC per dropped event. Route the counter path through a Unit event flow (MutableSharedFlow, capacity 1, DROP_OLDEST) collected with debounce(300): a burst collapses into one notification update and the last tick always renders. No numeric counter, so nothing overflows or wraps no matter how long the process runs; tryEmit never suspends or fails on Quartz IO threads. Connection-state updates keep their existing debounce so connect/disconnect still shows promptly. Fixes ngit issue nevent1qy28wumn8ghj7un9d3shjtnwva5hgtnyv4mqqg80gw6ushd8jyj6rt8qseup7hngj0xhqa2ms3dvam60lhc760zvkyq6uts9 --- .../nostrsigner/relays/AmberRelayStat.kt | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/relays/AmberRelayStat.kt b/app/src/main/java/com/greenart7c3/nostrsigner/relays/AmberRelayStat.kt index 1d22ae09..4fa2541d 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/relays/AmberRelayStat.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/relays/AmberRelayStat.kt @@ -25,6 +25,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.onEach @@ -39,6 +41,23 @@ class AmberRelayStats( var available = emptySet() var connected = emptySet() + // Counter updates arrive per relay event (NostrClientLoggerListener.onSent), + // which can burst at 30+ events/sec and trip Android's per-app notification + // rate limit (~5/sec): NotificationManager sheds every update and the status + // notification freezes on stale text until the burst ends. Emitting a tick + // defers notify() to the debounced collector started in + // createNotificationChannel(), so a burst collapses into one notification + // update. Connection-state changes keep their own (also debounced) + // collectors so connect/disconnect still shows promptly. + // Unit events instead of a numeric counter: nothing to overflow or wrap no + // matter how long the app runs. DROP_OLDEST + capacity 1 keeps only the + // newest tick, so tryEmit never fails and the last event of a burst always + // renders. + private val counterTick = MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + @OptIn(FlowPreview::class) @SuppressLint("MissingPermission") val relayStatus = combine(client.availableRelaysFlow(), client.connectedRelaysFlow()) { available, connected -> @@ -107,6 +126,14 @@ class AmberRelayStats( updateNotification() } } + Amber.instance.applicationIOScope.launch { + // Trailing-edge debounce for the counter path (addSent/addFailed): + // the last tick of a burst always renders, at most one notify per + // 300ms window. + counterTick.debounce(300).collect { + updateNotification() + } + } } private val innerCache = mutableMapOf() @@ -253,12 +280,16 @@ class AmberRelayStats( fun addSent(url: NormalizedRelayUrl) { get(url).addSent() - updateNotification() + scheduleNotificationUpdate() } fun addFailed(url: NormalizedRelayUrl) { get(url).addFailed() - updateNotification() + scheduleNotificationUpdate() + } + + private fun scheduleNotificationUpdate() { + counterTick.tryEmit(Unit) } }