From 96eb412fe52c7b703f1cc05aa440b2e928cf3df7 Mon Sep 17 00:00:00 2001 From: zarzet Date: Mon, 13 Jul 2026 20:18:27 +0700 Subject: [PATCH] perf(memory): release Go heap and decoded image caches on OS memory pressure --- .../kotlin/com/zarz/spotiflac/MainActivity.kt | 6 ++++++ go_backend/exports.go | 10 ++++++++++ lib/main.dart | 17 +++++++++++++++++ lib/services/platform_bridge.dart | 8 ++++++++ 4 files changed, 41 insertions(+) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index 77254873..e75aea3a 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -3043,6 +3043,12 @@ class MainActivity: FlutterFragmentActivity() { } result.success(null) } + "releaseMemory" -> { + withContext(Dispatchers.IO) { + Gobackend.releaseMemory() + } + result.success(null) + } "getLogCount" -> { val count = withContext(Dispatchers.IO) { Gobackend.getLogCount() diff --git a/go_backend/exports.go b/go_backend/exports.go index 4ea78fdf..12b13832 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -2,9 +2,19 @@ package gobackend import ( "encoding/json" + "runtime/debug" "strings" ) +// ReleaseMemory drops idle pooled extension runtimes, forces a GC, and +// returns freed heap to the OS. Called from the app on OS memory pressure and +// when backgrounded, so the Go side's RSS doesn't sit at its high-water mark +// after large downloads/tag writes. +func ReleaseMemory() { + drainAllIsolatedRuntimePools() + debug.FreeOSMemory() +} + func CheckAvailability(spotifyID, isrc string) (string, error) { client := NewSongLinkClient() availability, err := client.CheckTrackAvailability(spotifyID, isrc) diff --git a/lib/main.dart b/lib/main.dart index b47da80c..36407f2a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,7 @@ import 'package:spotiflac_android/providers/library_collections_provider.dart'; import 'package:spotiflac_android/providers/local_library_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/services/notification_service.dart'; +import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/services/share_intent_service.dart'; import 'package:spotiflac_android/services/cover_cache_manager.dart'; import 'package:spotiflac_android/utils/local_library_scan_prefs.dart'; @@ -157,9 +158,25 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> ref.read(downloadQueueProvider.notifier).flushQueuePersistence(), ); } + // Backgrounded: return the Go heap's high-water mark to the OS so the + // process is a smaller kill target. + unawaited(PlatformBridge.releaseNativeMemory()); } } + @override + void didHaveMemoryPressure() { + // OS memory pressure: drop decoded bitmaps (disk caches stay intact) and + // have the Go side release freed heap back to the OS. + final imageCache = PaintingBinding.instance.imageCache; + imageCache.clear(); + imageCache.clearLiveImages(); + if (CoverCacheManager.isInitialized) { + CoverCacheManager.instance.store.emptyMemoryCache(); + } + unawaited(PlatformBridge.releaseNativeMemory()); + } + void _initializeDeferredProviders() { _downloadHistoryWarmupTimer = _scheduleProviderWarmup( const Duration(milliseconds: 400), diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 30ae2212..09a11432 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -1202,6 +1202,14 @@ class PlatformBridge { await _channel.invokeMethod('clearLogs'); } + /// Ask the Go backend to GC and return freed heap to the OS. Best-effort: + /// safe to call on memory pressure or when the app is backgrounded. + static Future releaseNativeMemory() async { + try { + await _channel.invokeMethod('releaseMemory'); + } catch (_) {} + } + static Future getGoLogCount() async { final result = await _channel.invokeMethod('getLogCount'); return result as int;