From 72a8811c91e30b5854b6720f2bb2e7a7b298359c Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Sat, 18 Jul 2026 23:31:08 +0300 Subject: [PATCH] feat(trayapp): trim the heap after the popup is hidden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The popup keeps its composition alive while hidden so state survives hide/show — garbage from the visible session otherwise lingers for as long as the app idles in the tray, which is exactly the footprint users check in the task manager. Trigger a collection ~1.5s after the exit animation ends: no UI is on screen so the pause is invisible, reshowing cancels the pending delay (free debounce), and the call runs off the UI thread. --- .../composenativetray/tray/api/TrayApp.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/jvmMain/kotlin/dev/nucleusframework/composenativetray/tray/api/TrayApp.kt b/src/jvmMain/kotlin/dev/nucleusframework/composenativetray/tray/api/TrayApp.kt index 0fd7125..2290ac2 100644 --- a/src/jvmMain/kotlin/dev/nucleusframework/composenativetray/tray/api/TrayApp.kt +++ b/src/jvmMain/kotlin/dev/nucleusframework/composenativetray/tray/api/TrayApp.kt @@ -63,6 +63,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.painterResource import java.util.concurrent.atomic.AtomicLong @@ -121,6 +122,29 @@ private val defaultVerticalOffset = } } +private const val GC_AFTER_HIDE_DELAY_MS = 1_500L + +/** + * Trims the heap after the popup leaves the screen. The popup keeps its + * composition alive while hidden (so state survives hide/show), which means + * garbage from the visible session otherwise lingers for as long as the app + * idles in the tray — exactly the footprint users check in the task manager. + * Right after the exit animation there is no UI on screen, so the collection + * pause is invisible. + * + * Must be called from the visibility `LaunchedEffect`: reshowing the popup + * restarts the effect and cancels the [delay], debouncing rapid toggles for + * free. The collection runs off the UI thread so the tray loop only pays the + * stop-the-world portion. + */ +private suspend fun requestGcWhileHidden() { + delay(GC_AFTER_HIDE_DELAY_MS) + withContext(Dispatchers.Default) { + @Suppress("ExplicitGarbageCollectionCall") + System.gc() + } +} + // --------------------- Public API (overloads) --------------------- @Composable @@ -661,6 +685,7 @@ private fun TrayAppImplPanel( snapshotFlow { visibleState.isIdle && !visibleState.currentState }.first { it } popupOnScreen = false lastHiddenAtMs.set(System.currentTimeMillis()) + requestGcWhileHidden() } } } @@ -851,6 +876,7 @@ private fun NucleusApplicationScope.TrayAppImplWindow( snapshotFlow { visibleState.isIdle && !visibleState.currentState }.first { it } shouldShowWindow = false lastHiddenAtMs.set(System.currentTimeMillis()) + requestGcWhileHidden() } } }