perf(memory): drop disposable caches on pressure

This commit is contained in:
zarzet
2026-07-16 10:32:33 +07:00
parent c75139450a
commit ef89352d09
9 changed files with 100 additions and 3 deletions
@@ -3147,6 +3147,12 @@ class MainActivity: FlutterFragmentActivity() {
}
result.success(null)
}
"releaseMemoryUnderPressure" -> {
withContext(Dispatchers.IO) {
Gobackend.releaseMemoryUnderPressure()
}
result.success(null)
}
"setMetadataLanguage" -> {
val tag = call.argument<String>("tag") ?: ""
withContext(Dispatchers.IO) {
+7
View File
@@ -87,6 +87,13 @@ var (
coverFetch = fetchCoverBytes
)
func clearCoverMemoryCache() {
coverMu.Lock()
coverCache = map[string]*coverCacheEntry{}
coverCacheBytes = 0
coverMu.Unlock()
}
// fetchCoverCached returns cover bytes for a final URL, collapsing concurrent
// requests for the same URL into a single fetch (singleflight) and caching
// results in memory for the duration of an album batch. The returned slice is
+18
View File
@@ -36,7 +36,25 @@ func metadataAcceptLanguage() string {
// when backgrounded, so the Go side's RSS doesn't sit at its high-water mark
// after large downloads/tag writes.
func ReleaseMemory() {
releaseMemory(false)
}
// ReleaseMemoryUnderPressure additionally drops disposable live caches. It is
// reserved for an OS memory-pressure signal; ordinary backgrounding keeps
// network-backed caches warm.
func ReleaseMemoryUnderPressure() {
releaseMemory(true)
}
func releaseMemory(underPressure bool) {
drainAllIsolatedRuntimePools()
CloseIdleConnections()
if underPressure {
clearCoverMemoryCache()
globalLyricsCache.ClearAll()
clearPrivateIPCache()
clearExtensionHealthCache()
}
debug.FreeOSMemory()
}
+6
View File
@@ -52,6 +52,12 @@ var (
extensionHealthCache = map[string]cachedExtensionHealthResult{}
)
func clearExtensionHealthCache() {
extensionHealthCacheMu.Lock()
extensionHealthCache = map[string]cachedExtensionHealthResult{}
extensionHealthCacheMu.Unlock()
}
func CheckExtensionHealthJSON(extensionID string) (string, error) {
manager := getExtensionManager()
ext, err := manager.GetExtension(extensionID)
+6
View File
@@ -181,6 +181,12 @@ var (
privateIPCacheMu sync.RWMutex
)
func clearPrivateIPCache() {
privateIPCacheMu.Lock()
privateIPCache = make(map[string]privateIPCacheEntry)
privateIPCacheMu.Unlock()
}
func newExtensionRuntime(ext *loadedExtension) *extensionRuntime {
jar, _ := newSimpleCookieJar()
+44
View File
@@ -0,0 +1,44 @@
package gobackend
import (
"testing"
"time"
)
func TestReleaseMemoryUnderPressureClearsDisposableCaches(t *testing.T) {
clearCoverMemoryCache()
coverCachePut("https://example.com/cover.jpg", []byte("cover"))
globalLyricsCache.ClearAll()
globalLyricsCache.Set("artist", "track", 120, &LyricsResponse{PlainLyrics: "lyrics"})
privateIPCacheMu.Lock()
privateIPCache["example.com"] = privateIPCacheEntry{expiresAt: time.Now().Add(time.Hour)}
privateIPCacheMu.Unlock()
extensionHealthCacheMu.Lock()
extensionHealthCache["extension"] = cachedExtensionHealthResult{expiresAt: time.Now().Add(time.Hour)}
extensionHealthCacheMu.Unlock()
ReleaseMemoryUnderPressure()
coverMu.Lock()
coverEntries, coverBytes := len(coverCache), coverCacheBytes
coverMu.Unlock()
if coverEntries != 0 || coverBytes != 0 {
t.Fatalf("cover cache retained %d entries/%d bytes", coverEntries, coverBytes)
}
if globalLyricsCache.Size() != 0 {
t.Fatalf("lyrics cache retained %d entries", globalLyricsCache.Size())
}
privateIPCacheMu.RLock()
privateEntries := len(privateIPCache)
privateIPCacheMu.RUnlock()
if privateEntries != 0 {
t.Fatalf("private IP cache retained %d entries", privateEntries)
}
extensionHealthCacheMu.Lock()
healthEntries := len(extensionHealthCache)
extensionHealthCacheMu.Unlock()
if healthEntries != 0 {
t.Fatalf("health cache retained %d entries", healthEntries)
}
}
+8
View File
@@ -733,6 +733,14 @@ import Gobackend
case "clearLogs":
GobackendClearLogs()
return nil
case "releaseMemory":
GobackendReleaseMemory()
return nil
case "releaseMemoryUnderPressure":
GobackendReleaseMemoryUnderPressure()
return nil
case "getLogCount":
let response = GobackendGetLogCount()
+1 -1
View File
@@ -229,7 +229,7 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization>
if (CoverCacheManager.isInitialized) {
CoverCacheManager.instance.store.emptyMemoryCache();
}
unawaited(PlatformBridge.releaseNativeMemory());
unawaited(PlatformBridge.releaseNativeMemory(underPressure: true));
}
void _initializeDeferredProviders() {
+4 -2
View File
@@ -1168,9 +1168,11 @@ class PlatformBridge {
/// 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<void> releaseNativeMemory() async {
static Future<void> releaseNativeMemory({bool underPressure = false}) async {
try {
await _channel.invokeMethod('releaseMemory');
await _channel.invokeMethod(
underPressure ? 'releaseMemoryUnderPressure' : 'releaseMemory',
);
} catch (_) {}
}