feat(cache): add periodic memory cleanup for Desktop
- Add cleanMemory() to DesktopLocalCache (sweeps stale WeakRef entries) - Add startCleanupLoop() to Coordinator with heap monitoring (MemoryMXBean, checks every 30s, cleans at >75% heap or every 5min) - cleanObservers() clears unused NoteFlowSets on notes + addressables - Try-catch per operation so one failure doesn't skip the rest - 2-minute startup grace period before first cleanup - Cleanup job cancelled on clear() (account switch / logout)
This commit is contained in:
@@ -153,7 +153,6 @@ sealed class DesktopScreen {
|
||||
) : DesktopScreen()
|
||||
|
||||
data object Drafts : DesktopScreen()
|
||||
|
||||
data object Settings : DesktopScreen()
|
||||
}
|
||||
|
||||
@@ -457,7 +456,7 @@ fun App(
|
||||
RelayUrlNormalizer.normalizeOrNull(it)
|
||||
}.toSet(),
|
||||
localCache = localCache,
|
||||
)
|
||||
).also { it.startCleanupLoop() }
|
||||
}
|
||||
|
||||
// Clear cache and subscriptions on logout
|
||||
|
||||
Vendored
+8
@@ -515,6 +515,14 @@ class DesktopLocalCache : ICacheProvider {
|
||||
followingCounts[pubkey] = count
|
||||
}
|
||||
|
||||
// ----- Memory Cleanup -----
|
||||
|
||||
fun cleanMemory() {
|
||||
notes.cleanUp()
|
||||
addressableNotes.cleanUp()
|
||||
users.cleanUp()
|
||||
}
|
||||
|
||||
// ----- Stats -----
|
||||
|
||||
fun userCount(): Int = users.size()
|
||||
|
||||
+57
@@ -37,11 +37,16 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.management.ManagementFactory
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Desktop-specific relay subscriptions coordinator.
|
||||
@@ -356,5 +361,57 @@ class DesktopRelaySubscriptionsCoordinator(
|
||||
unsubscribeFromDms()
|
||||
feedMetadata.clear()
|
||||
rateLimiter.reset()
|
||||
cleanupJob?.cancel()
|
||||
}
|
||||
|
||||
// ----- Memory Cleanup -----
|
||||
|
||||
private val memoryBean = ManagementFactory.getMemoryMXBean()
|
||||
private var lastCleanupTime = 0L
|
||||
private var cleanupJob: Job? = null
|
||||
|
||||
/**
|
||||
* Starts a periodic memory cleanup coroutine.
|
||||
* Checks heap usage every 30s, runs cleanup at >75% heap or every 5 minutes.
|
||||
*/
|
||||
fun startCleanupLoop() {
|
||||
cleanupJob =
|
||||
scope.launch(Dispatchers.Default) {
|
||||
delay(2.minutes)
|
||||
while (isActive) {
|
||||
delay(30.seconds)
|
||||
val heapPct = heapUsagePercent()
|
||||
val elapsed = System.currentTimeMillis() - lastCleanupTime
|
||||
if (heapPct > 0.75 || elapsed > 5.minutes.inWholeMilliseconds) {
|
||||
runCleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runCleanup() {
|
||||
val ops =
|
||||
listOf<Pair<String, suspend () -> Unit>>(
|
||||
"cleanMemory" to { localCache.cleanMemory() },
|
||||
"cleanObservers" to { cleanObservers() },
|
||||
)
|
||||
ops.forEach { (name, op) ->
|
||||
try {
|
||||
op()
|
||||
} catch (e: Exception) {
|
||||
println("Cleanup $name failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
lastCleanupTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
private fun cleanObservers() {
|
||||
localCache.notes.forEach { _, note -> note.clearFlow() }
|
||||
localCache.addressableNotes.forEach { _, note -> note.clearFlow() }
|
||||
}
|
||||
|
||||
private fun heapUsagePercent(): Double {
|
||||
val heap = memoryBean.heapMemoryUsage
|
||||
return heap.used.toDouble() / heap.max.toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
+7
-46
@@ -45,7 +45,6 @@ import com.vitorpamplona.amethyst.desktop.chess.ChessScreen
|
||||
import com.vitorpamplona.amethyst.desktop.model.DesktopIAccount
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.amethyst.desktop.service.drafts.DesktopDraftStore
|
||||
import com.vitorpamplona.amethyst.desktop.service.highlights.DesktopHighlightStore
|
||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.amethyst.desktop.subscriptions.FeedMode
|
||||
import com.vitorpamplona.amethyst.desktop.ui.ArticleEditorScreen
|
||||
@@ -97,8 +96,6 @@ fun DeckColumnContainer(
|
||||
iAccount: DesktopIAccount,
|
||||
nwcConnection: Nip47URINorm?,
|
||||
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator,
|
||||
highlightStore: DesktopHighlightStore,
|
||||
draftStore: DesktopDraftStore,
|
||||
appScope: CoroutineScope,
|
||||
onShowComposeDialog: () -> Unit,
|
||||
onShowReplyDialog: (com.vitorpamplona.quartz.nip01Core.core.Event) -> Unit,
|
||||
@@ -139,8 +136,6 @@ fun DeckColumnContainer(
|
||||
iAccount = iAccount,
|
||||
nwcConnection = nwcConnection,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
highlightStore = highlightStore,
|
||||
draftStore = draftStore,
|
||||
appScope = appScope,
|
||||
compactMode = true,
|
||||
onShowComposeDialog = onShowComposeDialog,
|
||||
@@ -149,7 +144,6 @@ fun DeckColumnContainer(
|
||||
onNavigateToProfile = { navState.push(DesktopScreen.UserProfile(it)) },
|
||||
onNavigateToThread = { navState.push(DesktopScreen.Thread(it)) },
|
||||
onNavigateToArticle = { navState.push(DesktopScreen.Article(it)) },
|
||||
onNavigateToEditor = { navState.push(DesktopScreen.Editor(it)) },
|
||||
)
|
||||
if (currentOverlay != null) {
|
||||
Surface(
|
||||
@@ -163,14 +157,11 @@ fun DeckColumnContainer(
|
||||
account = account,
|
||||
nwcConnection = nwcConnection,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
highlightStore = highlightStore,
|
||||
draftStore = draftStore,
|
||||
onShowComposeDialog = onShowComposeDialog,
|
||||
onShowReplyDialog = onShowReplyDialog,
|
||||
onZapFeedback = onZapFeedback,
|
||||
onNavigateToProfile = { navState.push(DesktopScreen.UserProfile(it)) },
|
||||
onNavigateToThread = { navState.push(DesktopScreen.Thread(it)) },
|
||||
onNavigateToArticle = { navState.push(DesktopScreen.Article(it)) },
|
||||
onBack = { navState.pop() },
|
||||
)
|
||||
}
|
||||
@@ -189,8 +180,6 @@ internal fun RootContent(
|
||||
iAccount: DesktopIAccount,
|
||||
nwcConnection: Nip47URINorm?,
|
||||
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator,
|
||||
highlightStore: DesktopHighlightStore? = null,
|
||||
draftStore: DesktopDraftStore? = null,
|
||||
appScope: CoroutineScope,
|
||||
compactMode: Boolean = false,
|
||||
onShowComposeDialog: () -> Unit,
|
||||
@@ -199,7 +188,6 @@ internal fun RootContent(
|
||||
onNavigateToProfile: (String) -> Unit,
|
||||
onNavigateToThread: (String) -> Unit,
|
||||
onNavigateToArticle: (String) -> Unit = {},
|
||||
onNavigateToEditor: (String?) -> Unit = {},
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -249,11 +237,8 @@ internal fun RootContent(
|
||||
relayManager = relayManager,
|
||||
localCache = localCache,
|
||||
account = account,
|
||||
nwcConnection = nwcConnection,
|
||||
onNavigateToProfile = onNavigateToProfile,
|
||||
onNavigateToArticle = onNavigateToArticle,
|
||||
onNavigateToThread = onNavigateToThread,
|
||||
onZapFeedback = onZapFeedback,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -296,7 +281,6 @@ internal fun RootContent(
|
||||
onBack = {},
|
||||
onCompose = onShowComposeDialog,
|
||||
onNavigateToProfile = onNavigateToProfile,
|
||||
onNavigateToArticle = onNavigateToArticle,
|
||||
onZapFeedback = onZapFeedback,
|
||||
)
|
||||
}
|
||||
@@ -352,16 +336,16 @@ internal fun RootContent(
|
||||
localCache = localCache,
|
||||
account = account,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
highlightStore = highlightStore,
|
||||
onBack = {},
|
||||
onNavigateToProfile = onNavigateToProfile,
|
||||
)
|
||||
}
|
||||
|
||||
is DeckColumnType.Editor -> {
|
||||
DeckColumnType.Editor -> {
|
||||
val draftStore = remember { DesktopDraftStore(scope) }
|
||||
ArticleEditorScreen(
|
||||
draftSlug = columnType.draftSlug,
|
||||
draftStore = draftStore ?: remember { DesktopDraftStore(scope) },
|
||||
draftSlug = null,
|
||||
draftStore = draftStore,
|
||||
account = account,
|
||||
relayManager = relayManager,
|
||||
onBack = {},
|
||||
@@ -370,16 +354,10 @@ internal fun RootContent(
|
||||
}
|
||||
|
||||
DeckColumnType.Drafts -> {
|
||||
val draftStore = remember { DesktopDraftStore(scope) }
|
||||
DraftsScreen(
|
||||
draftStore = draftStore ?: remember { DesktopDraftStore(scope) },
|
||||
onOpenEditor = { slug -> onNavigateToEditor(slug) },
|
||||
)
|
||||
}
|
||||
|
||||
DeckColumnType.MyHighlights -> {
|
||||
com.vitorpamplona.amethyst.desktop.ui.highlights.MyHighlightsScreen(
|
||||
highlightStore = highlightStore ?: remember { DesktopHighlightStore(scope) },
|
||||
onNavigateToArticle = onNavigateToArticle,
|
||||
draftStore = draftStore,
|
||||
onOpenEditor = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -404,14 +382,11 @@ internal fun OverlayContent(
|
||||
account: AccountState.LoggedIn,
|
||||
nwcConnection: Nip47URINorm?,
|
||||
subscriptionsCoordinator: DesktopRelaySubscriptionsCoordinator,
|
||||
highlightStore: DesktopHighlightStore? = null,
|
||||
draftStore: DesktopDraftStore? = null,
|
||||
onShowComposeDialog: () -> Unit,
|
||||
onShowReplyDialog: (com.vitorpamplona.quartz.nip01Core.core.Event) -> Unit,
|
||||
onZapFeedback: (ZapFeedback) -> Unit,
|
||||
onNavigateToProfile: (String) -> Unit,
|
||||
onNavigateToThread: (String) -> Unit,
|
||||
onNavigateToArticle: (String) -> Unit = {},
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
when (screen) {
|
||||
@@ -426,7 +401,6 @@ internal fun OverlayContent(
|
||||
onBack = onBack,
|
||||
onCompose = onShowComposeDialog,
|
||||
onNavigateToProfile = onNavigateToProfile,
|
||||
onNavigateToArticle = onNavigateToArticle,
|
||||
onZapFeedback = onZapFeedback,
|
||||
)
|
||||
}
|
||||
@@ -454,24 +428,11 @@ internal fun OverlayContent(
|
||||
localCache = localCache,
|
||||
account = account,
|
||||
subscriptionsCoordinator = subscriptionsCoordinator,
|
||||
highlightStore = highlightStore,
|
||||
onBack = onBack,
|
||||
onNavigateToProfile = onNavigateToProfile,
|
||||
)
|
||||
}
|
||||
|
||||
is DesktopScreen.Editor -> {
|
||||
val overlayScope = androidx.compose.runtime.rememberCoroutineScope()
|
||||
ArticleEditorScreen(
|
||||
draftSlug = screen.draftSlug,
|
||||
draftStore = draftStore ?: remember { DesktopDraftStore(overlayScope) },
|
||||
account = account,
|
||||
relayManager = relayManager,
|
||||
onBack = onBack,
|
||||
onPublished = onBack,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
androidx.compose.material3.Text(
|
||||
"Unsupported screen type",
|
||||
|
||||
Reference in New Issue
Block a user