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/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/gestures.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:window_manager/window_manager.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
@@ -109,9 +109,14 @@ Future<void> _bootstrapApp() async {
// Needed for formatting dates in different locales // Needed for formatting dates in different locales
await initializeDateFormatting(savedLocale.languageCode, null); await initializeDateFormatting(savedLocale.languageCode, null);
// Configure image cache for large libraries // Configure image cache — keep budget modest to leave headroom for Skia decode buffers
PaintingBinding.instance.imageCache.maximumSize = 2000; // default 1000 if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
PaintingBinding.instance.imageCache.maximumSizeBytes = 300 << 20; // 300MB 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 // Initialize services in parallel where possible
final futures = <Future<void>>[]; 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) /// Last time server health probes ran from a resume event (cooldown for desktop)
DateTime _lastResumeProbe = DateTime(0); DateTime _lastResumeProbe = DateTime(0);
/// Periodic memory check timer for desktop platforms
Timer? _memoryCheckTimer;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); 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(); _serverManager = MultiServerManager();
_aggregationService = DataAggregationService(_serverManager); _aggregationService = DataAggregationService(_serverManager);
_appDatabase = AppDatabase(); _appDatabase = AppDatabase();
@@ -332,10 +351,23 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
@override @override
void dispose() { void dispose() {
_memoryCheckTimer?.cancel();
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
super.dispose(); 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 @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) { switch (state) {
@@ -357,8 +389,12 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
} }
case AppLifecycleState.paused: case AppLifecycleState.paused:
case AppLifecycleState.detached: case AppLifecycleState.detached:
// App went to background or is closing - end session
InAppReviewService.instance.endSession(); InAppReviewService.instance.endSession();
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
if (ProcessInfo.currentRss > 1024 * 1024 * 1024) { // 1GB
_evictImageCaches();
}
}
case AppLifecycleState.inactive: case AppLifecycleState.inactive:
case AppLifecycleState.hidden: case AppLifecycleState.hidden:
// Transitional states - don't trigger session events // Transitional states - don't trigger session events
+4
View File
@@ -72,6 +72,10 @@ class BifThumbnailService {
try { try {
final bytes = await client.downloadBifFile(partId); final bytes = await client.downloadBifFile(partId);
if (bytes == null || bytes.isEmpty) return; if (bytes == null || bytes.isEmpty) return;
if (bytes.length > 50 * 1024 * 1024) {
appLogger.w('BIF file too large (${bytes.length} bytes), skipping');
return;
}
_entries = await Isolate.run(() => _parseBifBytes(bytes)); _entries = await Isolate.run(() => _parseBifBytes(bytes));
} catch (e) { } catch (e) {
appLogger.w('BIF download/parse failed', error: e); appLogger.w('BIF download/parse failed', error: e);
+2 -2
View File
@@ -17,8 +17,8 @@ class PlexImageCacheManager extends CacheManager with ImageCacheManager {
: super( : super(
Config( Config(
_key, _key,
stalePeriod: const Duration(days: 30), stalePeriod: const Duration(days: 14),
maxNrOfCacheObjects: 5000, maxNrOfCacheObjects: 3000,
fileService: HttpFileService( fileService: HttpFileService(
httpClient: IOClient( httpClient: IOClient(
HttpClient()..maxConnectionsPerHost = 6, HttpClient()..maxConnectionsPerHost = 6,
+5 -5
View File
@@ -1,7 +1,9 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../models/hotkey_model.dart'; import '../models/hotkey_model.dart';
import 'image_cache_service.dart';
import 'package:plezy/utils/app_logger.dart'; import 'package:plezy/utils/app_logger.dart';
import '../i18n/strings.g.dart'; import '../i18n/strings.g.dart';
import '../models/mpv_config_models.dart'; import '../models/mpv_config_models.dart';
@@ -1386,11 +1388,9 @@ class SettingsService extends BaseSharedPreferencesService {
// Clear cache (for storage cleanup) // Clear cache (for storage cleanup)
Future<void> clearCache() async { Future<void> clearCache() async {
// This would be expanded to clear various cache directories PaintingBinding.instance.imageCache.clear();
// For now, we'll just clear any cache-related preferences PaintingBinding.instance.imageCache.clearLiveImages();
await Future.wait([ await PlexImageCacheManager.instance.emptyCache();
// Add cache clearing logic here
]);
} }
// Get all settings as a map for debugging/export // Get all settings as a map for debugging/export