diff --git a/lib/main.dart b/lib/main.dart index af73ec02..08bafdb9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 _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 = >[]; @@ -309,11 +314,25 @@ class _MainAppState extends State 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 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 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 diff --git a/lib/services/bif_thumbnail_service.dart b/lib/services/bif_thumbnail_service.dart index cc616f3e..4e897d30 100644 --- a/lib/services/bif_thumbnail_service.dart +++ b/lib/services/bif_thumbnail_service.dart @@ -72,6 +72,10 @@ class BifThumbnailService { try { final bytes = await client.downloadBifFile(partId); 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)); } catch (e) { appLogger.w('BIF download/parse failed', error: e); diff --git a/lib/services/image_cache_service.dart b/lib/services/image_cache_service.dart index 293a9d80..631146a0 100644 --- a/lib/services/image_cache_service.dart +++ b/lib/services/image_cache_service.dart @@ -17,8 +17,8 @@ class PlexImageCacheManager extends CacheManager with ImageCacheManager { : super( Config( _key, - stalePeriod: const Duration(days: 30), - maxNrOfCacheObjects: 5000, + stalePeriod: const Duration(days: 14), + maxNrOfCacheObjects: 3000, fileService: HttpFileService( httpClient: IOClient( HttpClient()..maxConnectionsPerHost = 6, diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index d75cff9e..55851531 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -1,7 +1,9 @@ import 'dart:convert'; import 'dart:io'; +import 'package:flutter/painting.dart'; import 'package:flutter/services.dart'; import '../models/hotkey_model.dart'; +import 'image_cache_service.dart'; import 'package:plezy/utils/app_logger.dart'; import '../i18n/strings.g.dart'; import '../models/mpv_config_models.dart'; @@ -1386,11 +1388,9 @@ class SettingsService extends BaseSharedPreferencesService { // Clear cache (for storage cleanup) Future clearCache() async { - // This would be expanded to clear various cache directories - // For now, we'll just clear any cache-related preferences - await Future.wait([ - // Add cache clearing logic here - ]); + PaintingBinding.instance.imageCache.clear(); + PaintingBinding.instance.imageCache.clearLiveImages(); + await PlexImageCacheManager.instance.emptyCache(); } // Get all settings as a map for debugging/export