mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 08:41:57 +00:00
removed aggressive cache cleaning
This commit is contained in:
parent
714226b6a5
commit
8178dfc215
7 changed files with 38 additions and 389 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { View, StyleSheet, Dimensions } from 'react-native';
|
import { View, StyleSheet, Dimensions } from 'react-native';
|
||||||
import { Image as ExpoImage } from 'expo-image';
|
import FastImage from '@d11/react-native-fast-image';
|
||||||
import { imageCacheService } from '../../services/imageCacheService';
|
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
|
||||||
interface OptimizedImageProps {
|
interface OptimizedImageProps {
|
||||||
|
|
@ -59,16 +58,14 @@ const OptimizedImage: React.FC<OptimizedImageProps> = ({
|
||||||
onLoad,
|
onLoad,
|
||||||
onError,
|
onError,
|
||||||
contentFit = 'cover',
|
contentFit = 'cover',
|
||||||
transition = 200,
|
transition = 0,
|
||||||
cachePolicy = 'memory'
|
cachePolicy = 'memory'
|
||||||
}) => {
|
}) => {
|
||||||
const [isLoaded, setIsLoaded] = useState(false);
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
const [hasError, setHasError] = useState(false);
|
const [hasError, setHasError] = useState(false);
|
||||||
const [isVisible, setIsVisible] = useState(!lazy);
|
const [isVisible, setIsVisible] = useState(!lazy);
|
||||||
const [recyclingKey] = useState(() => `${Math.random().toString(36).slice(2)}-${Date.now()}`);
|
|
||||||
const [optimizedUrl, setOptimizedUrl] = useState<string>('');
|
const [optimizedUrl, setOptimizedUrl] = useState<string>('');
|
||||||
const mountedRef = useRef(true);
|
const mountedRef = useRef(true);
|
||||||
const loadTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
|
|
||||||
// Extract URL from source
|
// Extract URL from source
|
||||||
const sourceUrl = typeof source === 'string' ? source : source?.uri || '';
|
const sourceUrl = typeof source === 'string' ? source : source?.uri || '';
|
||||||
|
|
@ -80,9 +77,6 @@ const OptimizedImage: React.FC<OptimizedImageProps> = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
mountedRef.current = false;
|
mountedRef.current = false;
|
||||||
if (loadTimeoutRef.current) {
|
|
||||||
clearTimeout(loadTimeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -97,7 +91,6 @@ const OptimizedImage: React.FC<OptimizedImageProps> = ({
|
||||||
// Lazy loading intersection observer simulation
|
// Lazy loading intersection observer simulation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lazy && !isVisible) {
|
if (lazy && !isVisible) {
|
||||||
// Simple lazy loading - load after a short delay to simulate intersection
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
if (mountedRef.current) {
|
if (mountedRef.current) {
|
||||||
setIsVisible(true);
|
setIsVisible(true);
|
||||||
|
|
@ -108,41 +101,22 @@ const OptimizedImage: React.FC<OptimizedImageProps> = ({
|
||||||
}
|
}
|
||||||
}, [lazy, isVisible, priority]);
|
}, [lazy, isVisible, priority]);
|
||||||
|
|
||||||
// Preload image with caching
|
// Preload image via FastImage
|
||||||
const preloadImage = useCallback(async () => {
|
const preloadImage = useCallback(async () => {
|
||||||
if (!optimizedUrl || !isVisible) return;
|
if (!optimizedUrl || !isVisible) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use our cache service to manage the image
|
await FastImage.preload([{ uri: optimizedUrl }]);
|
||||||
const cachedUrl = await imageCacheService.getCachedImageUrl(optimizedUrl);
|
if (!mountedRef.current) return;
|
||||||
|
setIsLoaded(true);
|
||||||
// Set a timeout for loading
|
onLoad?.();
|
||||||
loadTimeoutRef.current = setTimeout(() => {
|
|
||||||
if (mountedRef.current && !isLoaded) {
|
|
||||||
logger.warn(`[OptimizedImage] Load timeout for: ${optimizedUrl.substring(0, 50)}...`);
|
|
||||||
setHasError(true);
|
|
||||||
}
|
|
||||||
}, 10000); // 10 second timeout
|
|
||||||
|
|
||||||
// Skip prefetch to reduce memory pressure and heating
|
|
||||||
// await ExpoImage.prefetch(cachedUrl);
|
|
||||||
|
|
||||||
if (mountedRef.current) {
|
|
||||||
setIsLoaded(true);
|
|
||||||
if (loadTimeoutRef.current) {
|
|
||||||
clearTimeout(loadTimeoutRef.current);
|
|
||||||
loadTimeoutRef.current = null;
|
|
||||||
}
|
|
||||||
onLoad?.();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (mountedRef.current) {
|
if (!mountedRef.current) return;
|
||||||
logger.error(`[OptimizedImage] Failed to load: ${optimizedUrl.substring(0, 50)}...`, error);
|
logger.error(`[OptimizedImage] Failed to preload: ${optimizedUrl.substring(0, 50)}...`, error);
|
||||||
setHasError(true);
|
setHasError(true);
|
||||||
onError?.(error);
|
onError?.(error);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [optimizedUrl, isVisible, isLoaded, onLoad, onError]);
|
}, [optimizedUrl, isVisible, onLoad, onError]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible && optimizedUrl && !isLoaded && !hasError) {
|
if (isVisible && optimizedUrl && !isLoaded && !hasError) {
|
||||||
|
|
@ -158,26 +132,23 @@ const OptimizedImage: React.FC<OptimizedImageProps> = ({
|
||||||
// Show placeholder while loading or on error
|
// Show placeholder while loading or on error
|
||||||
if (!isLoaded || hasError) {
|
if (!isLoaded || hasError) {
|
||||||
return (
|
return (
|
||||||
<ExpoImage
|
<FastImage
|
||||||
source={{ uri: placeholder }}
|
source={{ uri: placeholder }}
|
||||||
style={style}
|
style={style}
|
||||||
contentFit={contentFit}
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
transition={0}
|
|
||||||
cachePolicy="memory"
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpoImage
|
<FastImage
|
||||||
source={{ uri: optimizedUrl }}
|
source={{
|
||||||
|
uri: optimizedUrl,
|
||||||
|
priority: priority === 'high' ? FastImage.priority.high : priority === 'low' ? FastImage.priority.low : FastImage.priority.normal,
|
||||||
|
cache: FastImage.cacheControl.immutable
|
||||||
|
}}
|
||||||
style={style}
|
style={style}
|
||||||
contentFit={contentFit}
|
resizeMode={contentFit === 'contain' ? FastImage.resizeMode.contain : contentFit === 'cover' ? FastImage.resizeMode.cover : FastImage.resizeMode.cover}
|
||||||
transition={transition}
|
|
||||||
cachePolicy={cachePolicy}
|
|
||||||
// Use a stable recycling key per component instance to keep textures alive between reuses
|
|
||||||
// This mitigates flicker on fast horizontal scrolls
|
|
||||||
recyclingKey={recyclingKey}
|
|
||||||
onLoad={() => {
|
onLoad={() => {
|
||||||
setIsLoaded(true);
|
setIsLoaded(true);
|
||||||
onLoad?.();
|
onLoad?.();
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,12 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
const [menuVisible, setMenuVisible] = useState(false);
|
const [menuVisible, setMenuVisible] = useState(false);
|
||||||
const [isWatched, setIsWatched] = useState(false);
|
const [isWatched, setIsWatched] = useState(false);
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Reset image error state when item changes, allowing for retry on re-render
|
||||||
|
setImageError(false);
|
||||||
|
}, [item.id, item.poster]);
|
||||||
|
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const { settings, isLoaded } = useSettings();
|
const { settings, isLoaded } = useSettings();
|
||||||
const posterRadius = typeof settings.posterBorderRadius === 'number' ? settings.posterBorderRadius : 12;
|
const posterRadius = typeof settings.posterBorderRadius === 'number' ? settings.posterBorderRadius : 12;
|
||||||
|
|
@ -245,6 +251,9 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
}}
|
}}
|
||||||
style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, borderRadius: posterRadius }]}
|
style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, borderRadius: posterRadius }]}
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
onLoad={() => {
|
||||||
|
setImageError(false);
|
||||||
|
}}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
if (__DEV__) console.warn('Image load error for:', item.poster);
|
if (__DEV__) console.warn('Image load error for:', item.poster);
|
||||||
setImageError(true);
|
setImageError(true);
|
||||||
|
|
@ -359,6 +368,8 @@ const styles = StyleSheet.create({
|
||||||
});
|
});
|
||||||
|
|
||||||
export default React.memo(ContentItem, (prev, next) => {
|
export default React.memo(ContentItem, (prev, next) => {
|
||||||
// Only re-render when the item ID changes (FastImage handles caching internally)
|
// Re-render when identity or poster changes. Caching is handled by FastImage.
|
||||||
return prev.item.id === next.item.id && prev.item.type === next.item.type;
|
if (prev.item.id !== next.item.id) return false;
|
||||||
|
if (prev.item.poster !== next.item.poster) return false;
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
@ -32,7 +32,6 @@ import { useSettings } from '../../hooks/useSettings';
|
||||||
import { TMDBService } from '../../services/tmdbService';
|
import { TMDBService } from '../../services/tmdbService';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { imageCacheService } from '../../services/imageCacheService';
|
|
||||||
|
|
||||||
interface FeaturedContentProps {
|
interface FeaturedContentProps {
|
||||||
featuredContent: StreamingContent | null;
|
featuredContent: StreamingContent | null;
|
||||||
|
|
@ -234,7 +233,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
imageCacheService.getCachedImageUrl(url),
|
FastImage.preload([{ uri: url }]),
|
||||||
timeout,
|
timeout,
|
||||||
]);
|
]);
|
||||||
imageCache[url] = true;
|
imageCache[url] = true;
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,6 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { Toast } from 'toastify-react-native';
|
import { Toast } from 'toastify-react-native';
|
||||||
import FirstTimeWelcome from '../components/FirstTimeWelcome';
|
import FirstTimeWelcome from '../components/FirstTimeWelcome';
|
||||||
import { imageCacheService } from '../services/imageCacheService';
|
|
||||||
import { HeaderVisibility } from '../contexts/HeaderVisibility';
|
import { HeaderVisibility } from '../contexts/HeaderVisibility';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ const TraktItem = React.memo(({ item, width, navigation, currentTheme }: { item:
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
const fetchPoster = async () => {
|
const fetchPoster = async () => {
|
||||||
if (item.images) {
|
if (item.images) {
|
||||||
const url = await TraktService.getTraktPosterUrlCached(item.images);
|
const url = TraktService.getTraktPosterUrl(item.images);
|
||||||
if (isMounted && url) {
|
if (isMounted && url) {
|
||||||
setPosterUrl(url);
|
setPosterUrl(url);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,291 +0,0 @@
|
||||||
import { logger } from '../utils/logger';
|
|
||||||
import FastImage from '@d11/react-native-fast-image';
|
|
||||||
import { AppState, AppStateStatus } from 'react-native';
|
|
||||||
|
|
||||||
interface CachedImage {
|
|
||||||
url: string;
|
|
||||||
localPath: string;
|
|
||||||
timestamp: number;
|
|
||||||
expiresAt: number;
|
|
||||||
size?: number; // Track approximate memory usage
|
|
||||||
accessCount: number; // Track usage frequency
|
|
||||||
lastAccessed: number; // Track last access time
|
|
||||||
}
|
|
||||||
|
|
||||||
class ImageCacheService {
|
|
||||||
private cache = new Map<string, CachedImage>();
|
|
||||||
private readonly CACHE_DURATION = Infinity; // Session-only: valid until app close
|
|
||||||
private readonly MAX_CACHE_SIZE = 25; // Further reduced maximum number of cached images
|
|
||||||
private readonly MAX_MEMORY_MB = 40; // Further reduced maximum memory usage in MB
|
|
||||||
private currentMemoryUsage = 0;
|
|
||||||
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
||||||
private appStateSubscription: any = null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
// Start cleanup interval every 15 minutes (more frequent cleanup to reduce memory pressure)
|
|
||||||
this.cleanupInterval = setInterval(() => {
|
|
||||||
this.performCleanup();
|
|
||||||
}, 15 * 60 * 1000);
|
|
||||||
|
|
||||||
// Reduce memory footprint when app goes to background
|
|
||||||
this.appStateSubscription = AppState.addEventListener('change', this.handleAppStateChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a cached image URL or cache the original if not present
|
|
||||||
*/
|
|
||||||
public async getCachedImageUrl(originalUrl: string): Promise<string> {
|
|
||||||
if (!originalUrl || originalUrl.includes('placeholder')) {
|
|
||||||
return originalUrl; // Don't cache placeholder images
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have a valid cached version
|
|
||||||
const cached = this.cache.get(originalUrl);
|
|
||||||
if (cached && cached.expiresAt > Date.now()) {
|
|
||||||
// Update access tracking
|
|
||||||
cached.accessCount++;
|
|
||||||
cached.lastAccessed = Date.now();
|
|
||||||
// Skip verbose logging to reduce CPU load
|
|
||||||
return cached.localPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check memory pressure before adding new entries (more lenient)
|
|
||||||
if (this.cache.size >= this.MAX_CACHE_SIZE * 0.95) {
|
|
||||||
// Skip verbose logging to reduce CPU load
|
|
||||||
return originalUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Estimate image size (rough approximation)
|
|
||||||
const estimatedSize = this.estimateImageSize(originalUrl);
|
|
||||||
|
|
||||||
const cachedImage: CachedImage = {
|
|
||||||
url: originalUrl,
|
|
||||||
localPath: originalUrl, // In production, this would be a local file path
|
|
||||||
timestamp: Date.now(),
|
|
||||||
expiresAt: Date.now() + this.CACHE_DURATION,
|
|
||||||
size: estimatedSize,
|
|
||||||
accessCount: 1,
|
|
||||||
lastAccessed: Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
this.cache.set(originalUrl, cachedImage);
|
|
||||||
this.currentMemoryUsage += estimatedSize;
|
|
||||||
this.enforceMemoryLimits();
|
|
||||||
|
|
||||||
// Skip verbose logging to reduce CPU load
|
|
||||||
return cachedImage.localPath;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[ImageCache] Failed to cache image:', error);
|
|
||||||
return originalUrl; // Fallback to original URL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if an image is cached
|
|
||||||
*/
|
|
||||||
public isCached(url: string): boolean {
|
|
||||||
const cached = this.cache.get(url);
|
|
||||||
return cached !== undefined && cached.expiresAt > Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log cache status (for debugging)
|
|
||||||
*/
|
|
||||||
public logCacheStatus(): void {
|
|
||||||
const stats = this.getCacheStats();
|
|
||||||
logger.log(`[ImageCache] 📊 Cache Status: ${stats.size} total, ${stats.expired} expired`);
|
|
||||||
|
|
||||||
// Log first 5 cached URLs for debugging
|
|
||||||
const entries = Array.from(this.cache.entries()).slice(0, 5);
|
|
||||||
entries.forEach(([url, cached]) => {
|
|
||||||
const isExpired = cached.expiresAt <= Date.now();
|
|
||||||
const timeLeft = Math.max(0, cached.expiresAt - Date.now()) / 1000 / 60; // minutes
|
|
||||||
logger.log(`[ImageCache] - ${url.substring(0, 60)}... (${isExpired ? 'EXPIRED' : `${timeLeft.toFixed(1)}m left`})`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear expired cache entries
|
|
||||||
*/
|
|
||||||
public clearExpiredCache(): void {
|
|
||||||
const now = Date.now();
|
|
||||||
for (const [url, cached] of this.cache.entries()) {
|
|
||||||
if (cached.expiresAt <= now) {
|
|
||||||
this.cache.delete(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all cached images
|
|
||||||
*/
|
|
||||||
public clearAllCache(): void {
|
|
||||||
this.cache.clear();
|
|
||||||
logger.log('[ImageCache] Cleared all cached images');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cache statistics
|
|
||||||
*/
|
|
||||||
public getCacheStats(): { size: number; expired: number } {
|
|
||||||
const now = Date.now();
|
|
||||||
let expired = 0;
|
|
||||||
|
|
||||||
for (const cached of this.cache.values()) {
|
|
||||||
if (cached.expiresAt <= now) {
|
|
||||||
expired++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
size: this.cache.size,
|
|
||||||
expired,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enforce maximum cache size by removing oldest entries
|
|
||||||
*/
|
|
||||||
private enforceMaxCacheSize(): void {
|
|
||||||
if (this.cache.size <= this.MAX_CACHE_SIZE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to array and sort by timestamp (oldest first)
|
|
||||||
const entries = Array.from(this.cache.entries()).sort(
|
|
||||||
(a, b) => a[1].timestamp - b[1].timestamp
|
|
||||||
);
|
|
||||||
|
|
||||||
// Remove oldest entries
|
|
||||||
const toRemove = this.cache.size - this.MAX_CACHE_SIZE;
|
|
||||||
for (let i = 0; i < toRemove; i++) {
|
|
||||||
this.cache.delete(entries[i][0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.log(`[ImageCache] Removed ${toRemove} old entries to enforce cache size limit`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enforce memory limits using LRU eviction
|
|
||||||
*/
|
|
||||||
private enforceMemoryLimits(): void {
|
|
||||||
const maxMemoryBytes = this.MAX_MEMORY_MB * 1024 * 1024;
|
|
||||||
|
|
||||||
if (this.currentMemoryUsage <= maxMemoryBytes) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by access frequency and recency (LRU)
|
|
||||||
const entries = Array.from(this.cache.entries()).sort((a, b) => {
|
|
||||||
const scoreA = a[1].accessCount * 0.3 + (Date.now() - a[1].lastAccessed) * 0.7;
|
|
||||||
const scoreB = b[1].accessCount * 0.3 + (Date.now() - b[1].lastAccessed) * 0.7;
|
|
||||||
return scoreB - scoreA; // Higher score = more likely to be evicted
|
|
||||||
});
|
|
||||||
|
|
||||||
let removedCount = 0;
|
|
||||||
for (const [url, cached] of entries) {
|
|
||||||
if (this.currentMemoryUsage <= maxMemoryBytes * 0.8) { // Leave 20% buffer
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cache.delete(url);
|
|
||||||
this.currentMemoryUsage -= cached.size || 0;
|
|
||||||
removedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip verbose memory eviction logging to reduce CPU load
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Estimate image size based on URL patterns
|
|
||||||
*/
|
|
||||||
private estimateImageSize(url: string): number {
|
|
||||||
// Rough estimates in bytes based on common image types
|
|
||||||
if (url.includes('poster')) return 150 * 1024; // 150KB for posters
|
|
||||||
if (url.includes('banner') || url.includes('backdrop')) return 300 * 1024; // 300KB for banners
|
|
||||||
if (url.includes('logo')) return 50 * 1024; // 50KB for logos
|
|
||||||
if (url.includes('thumb')) return 75 * 1024; // 75KB for thumbnails
|
|
||||||
return 200 * 1024; // Default 200KB
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if we should skip caching due to memory pressure
|
|
||||||
*/
|
|
||||||
private shouldSkipCaching(): boolean {
|
|
||||||
const maxMemoryBytes = this.MAX_MEMORY_MB * 1024 * 1024;
|
|
||||||
return this.currentMemoryUsage > maxMemoryBytes * 0.9 || this.cache.size >= this.MAX_CACHE_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform comprehensive cleanup
|
|
||||||
*/
|
|
||||||
private performCleanup(): void {
|
|
||||||
const initialSize = this.cache.size;
|
|
||||||
const initialMemory = this.currentMemoryUsage;
|
|
||||||
|
|
||||||
// Remove expired entries
|
|
||||||
this.clearExpiredCache();
|
|
||||||
|
|
||||||
// Recalculate memory usage
|
|
||||||
this.recalculateMemoryUsage();
|
|
||||||
|
|
||||||
// Enforce limits
|
|
||||||
this.enforceMemoryLimits();
|
|
||||||
this.enforceMaxCacheSize();
|
|
||||||
|
|
||||||
// Avoid clearing Expo's global memory cache to prevent re-decode churn
|
|
||||||
|
|
||||||
const finalSize = this.cache.size;
|
|
||||||
const finalMemory = this.currentMemoryUsage;
|
|
||||||
|
|
||||||
// Skip verbose cleanup logging to reduce CPU load
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recalculate memory usage from cache entries
|
|
||||||
*/
|
|
||||||
private recalculateMemoryUsage(): void {
|
|
||||||
this.currentMemoryUsage = 0;
|
|
||||||
for (const cached of this.cache.values()) {
|
|
||||||
this.currentMemoryUsage += cached.size || 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup resources
|
|
||||||
*/
|
|
||||||
public destroy(): void {
|
|
||||||
if (this.cleanupInterval) {
|
|
||||||
clearInterval(this.cleanupInterval);
|
|
||||||
this.cleanupInterval = null;
|
|
||||||
}
|
|
||||||
if (this.appStateSubscription) {
|
|
||||||
this.appStateSubscription.remove();
|
|
||||||
this.appStateSubscription = null;
|
|
||||||
}
|
|
||||||
this.clearAllCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleAppStateChange = (nextState: AppStateStatus) => {
|
|
||||||
if (nextState !== 'active') {
|
|
||||||
// On background/inactive, aggressively trim cache to 25% to reduce memory pressure
|
|
||||||
const targetSize = Math.floor(this.MAX_CACHE_SIZE * 0.25);
|
|
||||||
if (this.cache.size > targetSize) {
|
|
||||||
const entries = Array.from(this.cache.entries());
|
|
||||||
const toRemove = this.cache.size - targetSize;
|
|
||||||
for (let i = 0; i < toRemove; i++) {
|
|
||||||
const [url, cached] = entries[i];
|
|
||||||
this.cache.delete(url);
|
|
||||||
this.currentMemoryUsage -= cached.size || 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Force aggressive memory cleanup
|
|
||||||
this.enforceMemoryLimits();
|
|
||||||
// Clear any remaining memory pressure
|
|
||||||
this.currentMemoryUsage = Math.min(this.currentMemoryUsage, this.MAX_MEMORY_MB * 1024 * 1024 * 0.3);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const imageCacheService = new ImageCacheService();
|
|
||||||
|
|
@ -1176,7 +1176,7 @@ export class TraktService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract poster URL from Trakt images with basic caching
|
* Extract poster URL from Trakt images
|
||||||
*/
|
*/
|
||||||
public static getTraktPosterUrl(images?: TraktImages): string | null {
|
public static getTraktPosterUrl(images?: TraktImages): string | null {
|
||||||
if (!images || !images.poster || images.poster.length === 0) {
|
if (!images || !images.poster || images.poster.length === 0) {
|
||||||
|
|
@ -1185,36 +1185,7 @@ export class TraktService {
|
||||||
|
|
||||||
// Get the first poster and add https prefix
|
// Get the first poster and add https prefix
|
||||||
const posterPath = images.poster[0];
|
const posterPath = images.poster[0];
|
||||||
const fullUrl = posterPath.startsWith('http') ? posterPath : `https://${posterPath}`;
|
return posterPath.startsWith('http') ? posterPath : `https://${posterPath}`;
|
||||||
|
|
||||||
// Try to use cached version synchronously (basic cache check)
|
|
||||||
const isCached = imageCacheService.isCached(fullUrl);
|
|
||||||
if (isCached) {
|
|
||||||
logger.log(`[TraktService] 🎯 Using cached poster: ${fullUrl.substring(0, 60)}...`);
|
|
||||||
} else {
|
|
||||||
logger.log(`[TraktService] 📥 New poster URL: ${fullUrl.substring(0, 60)}...`);
|
|
||||||
// Queue for async caching
|
|
||||||
imageCacheService.getCachedImageUrl(fullUrl).catch(error => {
|
|
||||||
logger.error('[TraktService] Background caching failed:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return fullUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract poster URL from Trakt images with async caching
|
|
||||||
*/
|
|
||||||
public static async getTraktPosterUrlCached(images?: TraktImages): Promise<string | null> {
|
|
||||||
const url = this.getTraktPosterUrl(images);
|
|
||||||
if (!url) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await imageCacheService.getCachedImageUrl(url);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[TraktService] Failed to cache image:', error);
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -2029,17 +2000,6 @@ export class TraktService {
|
||||||
logger.error('[TraktService] DEBUG: Error fetching playback progress:', error);
|
logger.error('[TraktService] DEBUG: Error fetching playback progress:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Debug image cache status
|
|
||||||
*/
|
|
||||||
public static debugImageCache(): void {
|
|
||||||
try {
|
|
||||||
logger.log('[TraktService] === IMAGE CACHE DEBUG ===');
|
|
||||||
imageCacheService.logCacheStatus();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('[TraktService] Debug image cache failed:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a playback progress entry on Trakt by its playback `id`.
|
* Delete a playback progress entry on Trakt by its playback `id`.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue