fix: reduce image cache budgets and add memory pressure handling

This commit is contained in:
edde746
2026-03-17 09:48:18 +01:00
parent 4147bd0d86
commit 0fcdcf3c1f
4 changed files with 52 additions and 12 deletions
+41 -5
View File
@@ -2,7 +2,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';
import 'dart:io' show Platform;
import 'dart:io' show Platform, ProcessInfo;
import 'package:window_manager/window_manager.dart';
import 'package:provider/provider.dart';
import 'package:flutter_svg/flutter_svg.dart';
@@ -109,9 +109,14 @@ Future<void> _bootstrapApp() async {
// Needed for formatting dates in different locales
await initializeDateFormatting(savedLocale.languageCode, null);
// Configure image cache for large libraries
PaintingBinding.instance.imageCache.maximumSize = 2000; // default 1000
PaintingBinding.instance.imageCache.maximumSizeBytes = 300 << 20; // 300MB
// Configure image cache — keep budget modest to leave headroom for Skia decode buffers
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
PaintingBinding.instance.imageCache.maximumSize = 1000;
PaintingBinding.instance.imageCache.maximumSizeBytes = 150 << 20; // 150MB
} else {
PaintingBinding.instance.imageCache.maximumSize = 800;
PaintingBinding.instance.imageCache.maximumSizeBytes = 100 << 20; // 100MB
}
// Initialize services in parallel where possible
final futures = <Future<void>>[];
@@ -309,11 +314,25 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
/// Last time server health probes ran from a resume event (cooldown for desktop)
DateTime _lastResumeProbe = DateTime(0);
/// Periodic memory check timer for desktop platforms
Timer? _memoryCheckTimer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// On desktop, periodically check RSS and evict image cache if too high
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
_memoryCheckTimer = Timer.periodic(const Duration(seconds: 30), (_) {
final rss = ProcessInfo.currentRss;
if (rss > 1536 * 1024 * 1024) { // 1.5GB
appLogger.w('RSS high ($rss bytes), evicting image caches');
_evictImageCaches();
}
});
}
_serverManager = MultiServerManager();
_aggregationService = DataAggregationService(_serverManager);
_appDatabase = AppDatabase();
@@ -332,10 +351,23 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
@override
void dispose() {
_memoryCheckTimer?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didHaveMemoryPressure() {
super.didHaveMemoryPressure();
appLogger.w('System memory pressure, evicting image caches');
_evictImageCaches();
}
void _evictImageCaches() {
PaintingBinding.instance.imageCache.clear();
PaintingBinding.instance.imageCache.clearLiveImages();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
@@ -357,8 +389,12 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
}
case AppLifecycleState.paused:
case AppLifecycleState.detached:
// App went to background or is closing - end session
InAppReviewService.instance.endSession();
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
if (ProcessInfo.currentRss > 1024 * 1024 * 1024) { // 1GB
_evictImageCaches();
}
}
case AppLifecycleState.inactive:
case AppLifecycleState.hidden:
// Transitional states - don't trigger session events