mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
Calender screen optimziation which caused OOM.
This commit is contained in:
parent
bc2a15f81f
commit
2a118a17d4
10 changed files with 838 additions and 115 deletions
8
App.tsx
8
App.tsx
|
|
@ -32,6 +32,7 @@ import { useUpdatePopup } from './src/hooks/useUpdatePopup';
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import * as Sentry from '@sentry/react-native';
|
||||
import UpdateService from './src/services/updateService';
|
||||
import { memoryMonitorService } from './src/services/memoryMonitorService';
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://1a58bf436454d346e5852b7bfd3c95e8@o4509536317276160.ingest.de.sentry.io/4509536317734992',
|
||||
|
|
@ -81,7 +82,7 @@ const ThemedApp = () => {
|
|||
handleDismiss,
|
||||
} = useUpdatePopup();
|
||||
|
||||
// Check onboarding status and initialize update service
|
||||
// Check onboarding status and initialize services
|
||||
useEffect(() => {
|
||||
const initializeApp = async () => {
|
||||
try {
|
||||
|
|
@ -91,6 +92,11 @@ const ThemedApp = () => {
|
|||
|
||||
// Initialize update service
|
||||
await UpdateService.initialize();
|
||||
|
||||
// Initialize memory monitoring service to prevent OutOfMemoryError
|
||||
memoryMonitorService; // Just accessing it starts the monitoring
|
||||
console.log('Memory monitoring service initialized');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error initializing app:', error);
|
||||
// Default to showing onboarding if we can't check
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
----------------------------568300859475270590089014
|
||||
Content-Disposition: form-data; name="directive"
|
||||
Content-Type: application/json
|
||||
content-type: application/json; charset=utf-8
|
||||
|
||||
{"type":"noUpdateAvailable"}
|
||||
----------------------------568300859475270590089014--
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
----------------------------694338510290346396309710
|
||||
Content-Disposition: form-data; name="directive"
|
||||
Content-Type: application/json
|
||||
content-type: application/json; charset=utf-8
|
||||
|
||||
{"type":"noUpdateAvailable"}
|
||||
----------------------------694338510290346396309710--
|
||||
|
|
@ -15,13 +15,13 @@ import { LinearGradient } from 'expo-linear-gradient';
|
|||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { useTraktContext } from '../../contexts/TraktContext';
|
||||
import { stremioService } from '../../services/stremioService';
|
||||
import { tmdbService } from '../../services/tmdbService';
|
||||
import { useLibrary } from '../../hooks/useLibrary';
|
||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||
import { parseISO, isThisWeek, format, isAfter, isBefore } from 'date-fns';
|
||||
import Animated, { FadeIn } from 'react-native-reanimated';
|
||||
import { useCalendarData } from '../../hooks/useCalendarData';
|
||||
import { memoryManager } from '../../utils/memoryManager';
|
||||
import { tmdbService } from '../../services/tmdbService';
|
||||
|
||||
// Compute base sizes; actual tablet sizes will be adjusted inside component for responsiveness
|
||||
const { width } = Dimensions.get('window');
|
||||
|
|
@ -46,15 +46,6 @@ interface ThisWeekEpisode {
|
|||
|
||||
export const ThisWeekSection = React.memo(() => {
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { libraryItems, loading: libraryLoading } = useLibrary();
|
||||
const {
|
||||
isAuthenticated: traktAuthenticated,
|
||||
isLoading: traktLoading,
|
||||
watchedShows,
|
||||
watchlistShows,
|
||||
continueWatching,
|
||||
loadAllCollections
|
||||
} = useTraktContext();
|
||||
const { currentTheme } = useTheme();
|
||||
const { calendarData, loading } = useCalendarData();
|
||||
|
||||
|
|
@ -64,13 +55,17 @@ export const ThisWeekSection = React.memo(() => {
|
|||
const computedItemWidth = useMemo(() => (isTablet ? Math.min(deviceWidth * 0.46, 560) : ITEM_WIDTH), [isTablet, deviceWidth]);
|
||||
const computedItemHeight = useMemo(() => (isTablet ? 220 : ITEM_HEIGHT), [isTablet]);
|
||||
|
||||
// Use the already memory-optimized calendar data instead of fetching separately
|
||||
const thisWeekEpisodes = useMemo(() => {
|
||||
const thisWeekSection = calendarData.find(section => section.title === 'This Week');
|
||||
if (!thisWeekSection) return [];
|
||||
|
||||
return thisWeekSection.data.map(episode => ({
|
||||
// Limit episodes to prevent memory issues and add release status
|
||||
const episodes = memoryManager.limitArraySize(thisWeekSection.data, 20); // Limit to 20 for home screen
|
||||
|
||||
return episodes.map(episode => ({
|
||||
...episode,
|
||||
isReleased: isBefore(parseISO(episode.releaseDate), new Date()),
|
||||
isReleased: episode.releaseDate ? isBefore(parseISO(episode.releaseDate), new Date()) : false,
|
||||
}));
|
||||
}, [calendarData]);
|
||||
|
||||
|
|
@ -104,8 +99,9 @@ export const ThisWeekSection = React.memo(() => {
|
|||
}
|
||||
|
||||
const renderEpisodeItem = ({ item, index }: { item: ThisWeekEpisode, index: number }) => {
|
||||
const releaseDate = parseISO(item.releaseDate);
|
||||
const formattedDate = format(releaseDate, 'E, MMM d');
|
||||
// Handle episodes without release dates gracefully
|
||||
const releaseDate = item.releaseDate ? parseISO(item.releaseDate) : null;
|
||||
const formattedDate = releaseDate ? format(releaseDate, 'E, MMM d') : 'TBA';
|
||||
const isReleased = item.isReleased;
|
||||
|
||||
// Use episode still image if available, fallback to series poster
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { robustCalendarCache } from '../services/robustCalendarCache';
|
|||
import { stremioService } from '../services/stremioService';
|
||||
import { tmdbService } from '../services/tmdbService';
|
||||
import { logger } from '../utils/logger';
|
||||
import { memoryManager } from '../utils/memoryManager';
|
||||
import { parseISO, isBefore, isAfter, startOfToday, addWeeks, isThisWeek } from 'date-fns';
|
||||
import { StreamingContent } from '../services/catalogService';
|
||||
|
||||
|
|
@ -53,6 +54,9 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
|||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Check memory pressure and cleanup if needed
|
||||
memoryManager.checkMemoryPressure();
|
||||
|
||||
if (!forceRefresh) {
|
||||
const cachedData = await robustCalendarCache.getCachedCalendarData(
|
||||
libraryItems,
|
||||
|
|
@ -138,47 +142,64 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
|||
}
|
||||
}
|
||||
|
||||
// Limit the number of series to prevent memory overflow
|
||||
const maxSeries = 100; // Reasonable limit to prevent OOM
|
||||
if (allSeries.length > maxSeries) {
|
||||
logger.warn(`[CalendarData] Too many series (${allSeries.length}), limiting to ${maxSeries} to prevent memory issues`);
|
||||
allSeries = allSeries.slice(0, maxSeries);
|
||||
}
|
||||
|
||||
logger.log(`[CalendarData] Total series to check: ${allSeries.length} (Library: ${librarySeries.length}, Trakt: ${allSeries.length - librarySeries.length})`);
|
||||
|
||||
let allEpisodes: CalendarEpisode[] = [];
|
||||
let seriesWithoutEpisodes: CalendarEpisode[] = [];
|
||||
|
||||
for (const series of allSeries) {
|
||||
try {
|
||||
const metadata = await stremioService.getMetaDetails(series.type, series.id);
|
||||
|
||||
if (metadata?.videos && metadata.videos.length > 0) {
|
||||
const today = startOfToday();
|
||||
const fourWeeksLater = addWeeks(today, 4);
|
||||
const twoWeeksAgo = addWeeks(today, -2);
|
||||
// Process series in memory-efficient batches to prevent OOM
|
||||
const processedSeries = await memoryManager.processArrayInBatches(
|
||||
allSeries,
|
||||
async (series: StreamingContent, index: number) => {
|
||||
try {
|
||||
// Use the new memory-efficient method to fetch only upcoming episodes
|
||||
const episodeData = await stremioService.getUpcomingEpisodes(series.type, series.id, {
|
||||
daysBack: 14, // 2 weeks back
|
||||
daysAhead: 28, // 4 weeks ahead
|
||||
maxEpisodes: 25, // Limit episodes per series
|
||||
});
|
||||
|
||||
const tmdbId = await tmdbService.findTMDBIdByIMDB(series.id);
|
||||
let tmdbEpisodes: { [key: string]: any } = {};
|
||||
|
||||
if (tmdbId) {
|
||||
const allTMDBEpisodes = await tmdbService.getAllEpisodes(tmdbId);
|
||||
Object.values(allTMDBEpisodes).forEach(seasonEpisodes => {
|
||||
seasonEpisodes.forEach(episode => {
|
||||
const key = `${episode.season_number}:${episode.episode_number}`;
|
||||
tmdbEpisodes[key] = episode;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const upcomingEpisodes = metadata.videos
|
||||
.filter(video => {
|
||||
if (!video.released) return false;
|
||||
const releaseDate = parseISO(video.released);
|
||||
return isBefore(releaseDate, fourWeeksLater) && isAfter(releaseDate, twoWeeksAgo);
|
||||
})
|
||||
.map(video => {
|
||||
if (episodeData && episodeData.episodes.length > 0) {
|
||||
const tmdbId = await tmdbService.findTMDBIdByIMDB(series.id);
|
||||
let tmdbEpisodes: { [key: string]: any } = {};
|
||||
|
||||
// Only fetch TMDB data if we need it and limit it
|
||||
if (tmdbId && episodeData.episodes.length > 0) {
|
||||
try {
|
||||
// Get only current and next season to limit memory usage
|
||||
const seasons = [...new Set(episodeData.episodes.map(ep => ep.season || 1))];
|
||||
const limitedSeasons = seasons.slice(0, 3); // Limit to 3 seasons max
|
||||
|
||||
for (const seasonNum of limitedSeasons) {
|
||||
const seasonEpisodes = await tmdbService.getSeasonDetails(tmdbId, seasonNum);
|
||||
if (seasonEpisodes?.episodes) {
|
||||
seasonEpisodes.episodes.forEach((episode: any) => {
|
||||
const key = `${episode.season_number}:${episode.episode_number}`;
|
||||
tmdbEpisodes[key] = episode;
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (tmdbError) {
|
||||
logger.warn(`[CalendarData] TMDB fetch failed for ${series.name}, continuing without additional metadata`);
|
||||
}
|
||||
}
|
||||
|
||||
// Transform episodes with memory-efficient processing
|
||||
const transformedEpisodes = episodeData.episodes.map(video => {
|
||||
const tmdbEpisode = tmdbEpisodes[`${video.season}:${video.episode}`] || {};
|
||||
return {
|
||||
id: video.id,
|
||||
seriesId: series.id,
|
||||
title: tmdbEpisode.name || video.title || `Episode ${video.episode}`,
|
||||
seriesName: series.name || metadata.name,
|
||||
poster: series.poster || metadata.poster || '',
|
||||
seriesName: series.name || episodeData.seriesName,
|
||||
poster: series.poster || episodeData.poster || '',
|
||||
releaseDate: video.released,
|
||||
season: video.season || 0,
|
||||
episode: video.episode || 0,
|
||||
|
|
@ -188,25 +209,89 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
|||
season_poster_path: tmdbEpisode.season_poster_path || null
|
||||
};
|
||||
});
|
||||
|
||||
if (upcomingEpisodes.length > 0) {
|
||||
allEpisodes = [...allEpisodes, ...upcomingEpisodes];
|
||||
|
||||
// Clear references to help garbage collection
|
||||
memoryManager.clearObjects(tmdbEpisodes);
|
||||
|
||||
return { type: 'episodes', data: transformedEpisodes };
|
||||
} else {
|
||||
seriesWithoutEpisodes.push({ id: series.id, seriesId: series.id, title: 'No upcoming episodes', seriesName: series.name || (metadata?.name || ''), poster: series.poster || (metadata?.poster || ''), releaseDate: '', season: 0, episode: 0, overview: '', vote_average: 0, still_path: null, season_poster_path: null });
|
||||
return {
|
||||
type: 'no-episodes',
|
||||
data: {
|
||||
id: series.id,
|
||||
seriesId: series.id,
|
||||
title: 'No upcoming episodes',
|
||||
seriesName: series.name || episodeData?.seriesName || '',
|
||||
poster: series.poster || episodeData?.poster || '',
|
||||
releaseDate: '',
|
||||
season: 0,
|
||||
episode: 0,
|
||||
overview: '',
|
||||
vote_average: 0,
|
||||
still_path: null,
|
||||
season_poster_path: null
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
seriesWithoutEpisodes.push({ id: series.id, seriesId: series.id, title: 'No upcoming episodes', seriesName: series.name || (metadata?.name || ''), poster: series.poster || (metadata?.poster || ''), releaseDate: '', season: 0, episode: 0, overview: '', vote_average: 0, still_path: null, season_poster_path: null });
|
||||
} catch (error) {
|
||||
logger.error(`[CalendarData] Error fetching episodes for ${series.name}:`, error);
|
||||
return {
|
||||
type: 'no-episodes',
|
||||
data: {
|
||||
id: series.id,
|
||||
seriesId: series.id,
|
||||
title: 'No upcoming episodes',
|
||||
seriesName: series.name || '',
|
||||
poster: series.poster || '',
|
||||
releaseDate: '',
|
||||
season: 0,
|
||||
episode: 0,
|
||||
overview: '',
|
||||
vote_average: 0,
|
||||
still_path: null,
|
||||
season_poster_path: null
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error fetching episodes for ${series.name}:`, error);
|
||||
},
|
||||
5, // Small batch size to prevent memory spikes
|
||||
100 // Small delay between batches
|
||||
);
|
||||
|
||||
// Process results and separate episodes from no-episode series
|
||||
for (const result of processedSeries) {
|
||||
if (result.type === 'episodes' && Array.isArray(result.data)) {
|
||||
allEpisodes.push(...result.data);
|
||||
} else if (result.type === 'no-episodes') {
|
||||
seriesWithoutEpisodes.push(result.data as CalendarEpisode);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear processed series to free memory
|
||||
memoryManager.clearObjects(processedSeries);
|
||||
|
||||
// Limit total episodes to prevent memory overflow
|
||||
allEpisodes = memoryManager.limitArraySize(allEpisodes, 500);
|
||||
seriesWithoutEpisodes = memoryManager.limitArraySize(seriesWithoutEpisodes, 100);
|
||||
|
||||
// Sort episodes by release date
|
||||
allEpisodes.sort((a, b) => new Date(a.releaseDate).getTime() - new Date(b.releaseDate).getTime());
|
||||
|
||||
const thisWeekEpisodes = allEpisodes.filter(ep => isThisWeek(parseISO(ep.releaseDate)));
|
||||
const upcomingEpisodes = allEpisodes.filter(ep => isAfter(parseISO(ep.releaseDate), new Date()) && !isThisWeek(parseISO(ep.releaseDate)));
|
||||
const recentEpisodes = allEpisodes.filter(ep => isBefore(parseISO(ep.releaseDate), new Date()) && !isThisWeek(parseISO(ep.releaseDate)));
|
||||
// Use memory-efficient filtering
|
||||
const thisWeekEpisodes = await memoryManager.filterLargeArray(
|
||||
allEpisodes,
|
||||
ep => isThisWeek(parseISO(ep.releaseDate))
|
||||
);
|
||||
|
||||
const upcomingEpisodes = await memoryManager.filterLargeArray(
|
||||
allEpisodes,
|
||||
ep => isAfter(parseISO(ep.releaseDate), new Date()) && !isThisWeek(parseISO(ep.releaseDate))
|
||||
);
|
||||
|
||||
const recentEpisodes = await memoryManager.filterLargeArray(
|
||||
allEpisodes,
|
||||
ep => isBefore(parseISO(ep.releaseDate), new Date()) && !isThisWeek(parseISO(ep.releaseDate))
|
||||
);
|
||||
|
||||
const sections: CalendarSection[] = [];
|
||||
if (thisWeekEpisodes.length > 0) sections.push({ title: 'This Week', data: thisWeekEpisodes });
|
||||
|
|
@ -216,6 +301,9 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
|||
|
||||
setCalendarData(sections);
|
||||
|
||||
// Clear large arrays to help garbage collection
|
||||
memoryManager.clearObjects(allEpisodes, thisWeekEpisodes, upcomingEpisodes, recentEpisodes);
|
||||
|
||||
await robustCalendarCache.setCachedCalendarData(
|
||||
sections,
|
||||
libraryItems,
|
||||
|
|
@ -223,7 +311,7 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
|||
);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error fetching calendar data:', error);
|
||||
logger.error('[CalendarData] Error fetching calendar data:', error);
|
||||
await robustCalendarCache.setCachedCalendarData(
|
||||
[],
|
||||
libraryItems,
|
||||
|
|
@ -231,6 +319,8 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
|||
true
|
||||
);
|
||||
} finally {
|
||||
// Force garbage collection after processing
|
||||
memoryManager.forceGarbageCollection();
|
||||
setLoading(false);
|
||||
}
|
||||
}, [libraryItems, traktAuthenticated, watchlistShows, continueWatching, watchedShows]);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import Animated, { FadeIn } from 'react-native-reanimated';
|
|||
import { CalendarSection as CalendarSectionComponent } from '../components/calendar/CalendarSection';
|
||||
import { tmdbService } from '../services/tmdbService';
|
||||
import { logger } from '../utils/logger';
|
||||
import { memoryManager } from '../utils/memoryManager';
|
||||
import { useCalendarData } from '../hooks/useCalendarData';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
|
@ -73,6 +74,8 @@ const CalendarScreen = () => {
|
|||
|
||||
const onRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
// Check memory pressure before refresh
|
||||
memoryManager.checkMemoryPressure();
|
||||
refresh(true);
|
||||
setRefreshing(false);
|
||||
}, [refresh]);
|
||||
|
|
@ -206,9 +209,14 @@ const CalendarScreen = () => {
|
|||
</View>
|
||||
);
|
||||
|
||||
// Process all episodes once data is loaded
|
||||
const allEpisodes = calendarData.reduce((acc: CalendarEpisode[], section: CalendarSection) =>
|
||||
[...acc, ...section.data], [] as CalendarEpisode[]);
|
||||
// Process all episodes once data is loaded - using memory-efficient approach
|
||||
const allEpisodes = React.useMemo(() => {
|
||||
const episodes = calendarData.reduce((acc: CalendarEpisode[], section: CalendarSection) =>
|
||||
[...acc, ...section.data], [] as CalendarEpisode[]);
|
||||
|
||||
// Limit episodes to prevent memory issues in large libraries
|
||||
return memoryManager.limitArraySize(episodes, 1000);
|
||||
}, [calendarData]);
|
||||
|
||||
// Log when rendering with relevant state info
|
||||
logger.log(`[Calendar] Rendering: loading=${loading}, calendarData sections=${calendarData.length}, allEpisodes=${allEpisodes.length}`);
|
||||
|
|
|
|||
258
src/services/memoryMonitorService.ts
Normal file
258
src/services/memoryMonitorService.ts
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
import { AppState, AppStateStatus } from 'react-native';
|
||||
import { logger } from '../utils/logger';
|
||||
import { memoryManager } from '../utils/memoryManager';
|
||||
|
||||
/**
|
||||
* Global memory monitoring service to prevent OutOfMemoryError
|
||||
* Monitors app state changes and automatically manages memory
|
||||
*/
|
||||
class MemoryMonitorService {
|
||||
private static instance: MemoryMonitorService;
|
||||
private appStateSubscription: any = null;
|
||||
private memoryCheckInterval: NodeJS.Timeout | null = null;
|
||||
private backgroundCleanupInterval: NodeJS.Timeout | null = null;
|
||||
private lastMemoryWarning: number = 0;
|
||||
private readonly MEMORY_CHECK_INTERVAL = 30 * 1000; // 30 seconds
|
||||
private readonly BACKGROUND_CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
private readonly MEMORY_WARNING_COOLDOWN = 60 * 1000; // 1 minute
|
||||
|
||||
private constructor() {
|
||||
this.startMonitoring();
|
||||
}
|
||||
|
||||
static getInstance(): MemoryMonitorService {
|
||||
if (!MemoryMonitorService.instance) {
|
||||
MemoryMonitorService.instance = new MemoryMonitorService();
|
||||
}
|
||||
return MemoryMonitorService.instance;
|
||||
}
|
||||
|
||||
private startMonitoring(): void {
|
||||
// Monitor app state changes
|
||||
this.appStateSubscription = AppState.addEventListener('change', this.handleAppStateChange);
|
||||
|
||||
// Periodic memory checks
|
||||
this.memoryCheckInterval = setInterval(() => {
|
||||
this.performMemoryCheck();
|
||||
}, this.MEMORY_CHECK_INTERVAL);
|
||||
|
||||
// Background cleanup
|
||||
this.backgroundCleanupInterval = setInterval(() => {
|
||||
this.performBackgroundCleanup();
|
||||
}, this.BACKGROUND_CLEANUP_INTERVAL);
|
||||
|
||||
logger.log('[MemoryMonitor] Started memory monitoring service');
|
||||
}
|
||||
|
||||
private handleAppStateChange = (nextAppState: AppStateStatus) => {
|
||||
try {
|
||||
switch (nextAppState) {
|
||||
case 'background':
|
||||
// App going to background - aggressive cleanup
|
||||
logger.log('[MemoryMonitor] App backgrounded, performing aggressive cleanup');
|
||||
this.performAggressiveCleanup();
|
||||
break;
|
||||
|
||||
case 'active':
|
||||
// App coming to foreground - light cleanup
|
||||
logger.log('[MemoryMonitor] App activated, performing light cleanup');
|
||||
memoryManager.checkMemoryPressure();
|
||||
break;
|
||||
|
||||
case 'inactive':
|
||||
// App becoming inactive - medium cleanup
|
||||
memoryManager.forceGarbageCollection();
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[MemoryMonitor] Error handling app state change:', error);
|
||||
}
|
||||
};
|
||||
|
||||
private performMemoryCheck(): void {
|
||||
try {
|
||||
// Check if we should perform cleanup
|
||||
const shouldCleanup = memoryManager.checkMemoryPressure();
|
||||
|
||||
if (shouldCleanup) {
|
||||
logger.log('[MemoryMonitor] Memory pressure detected, performing cleanup');
|
||||
}
|
||||
|
||||
// Detect potential memory issues
|
||||
this.detectMemoryIssues();
|
||||
} catch (error) {
|
||||
logger.error('[MemoryMonitor] Error during memory check:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private detectMemoryIssues(): void {
|
||||
try {
|
||||
// Check for large object accumulation indicators
|
||||
const now = Date.now();
|
||||
|
||||
// Simulate memory pressure detection (in a real app, you might check actual memory usage)
|
||||
// For React Native, we can't directly access memory stats, so we use heuristics
|
||||
|
||||
// Check if we should issue a memory warning
|
||||
if (now - this.lastMemoryWarning > this.MEMORY_WARNING_COOLDOWN) {
|
||||
// In a production app, you might want to track things like:
|
||||
// - Number of React components mounted
|
||||
// - Size of Redux store
|
||||
// - Number of network requests in flight
|
||||
// - Image cache size
|
||||
|
||||
// For this implementation, we'll trigger preventive cleanup periodically
|
||||
if (Math.random() < 0.1) { // 10% chance to trigger preventive cleanup
|
||||
this.issueMemoryWarning();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[MemoryMonitor] Error detecting memory issues:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private issueMemoryWarning(): void {
|
||||
const now = Date.now();
|
||||
this.lastMemoryWarning = now;
|
||||
|
||||
logger.warn('[MemoryMonitor] Memory usage warning - performing preventive cleanup');
|
||||
|
||||
// Perform immediate cleanup
|
||||
this.performAggressiveCleanup();
|
||||
}
|
||||
|
||||
private performBackgroundCleanup(): void {
|
||||
try {
|
||||
logger.log('[MemoryMonitor] Performing scheduled background cleanup');
|
||||
|
||||
// Force garbage collection
|
||||
memoryManager.forceGarbageCollection();
|
||||
|
||||
// Clear any global caches that might have accumulated
|
||||
this.clearGlobalCaches();
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[MemoryMonitor] Error during background cleanup:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private performAggressiveCleanup(): void {
|
||||
try {
|
||||
logger.log('[MemoryMonitor] Performing aggressive memory cleanup');
|
||||
|
||||
// Multiple garbage collection cycles
|
||||
for (let i = 0; i < 3; i++) {
|
||||
memoryManager.forceGarbageCollection();
|
||||
// Small delay between GC cycles
|
||||
setTimeout(() => {}, 100);
|
||||
}
|
||||
|
||||
// Clear all possible caches
|
||||
this.clearGlobalCaches();
|
||||
|
||||
// Clear image caches if available
|
||||
this.clearImageCaches();
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[MemoryMonitor] Error during aggressive cleanup:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private clearGlobalCaches(): void {
|
||||
try {
|
||||
// Clear any global caches your app might have
|
||||
if (global && global.__APP_CACHE__) {
|
||||
global.__APP_CACHE__ = {};
|
||||
}
|
||||
|
||||
if (global && global.__METADATA_CACHE__) {
|
||||
global.__METADATA_CACHE__ = {};
|
||||
}
|
||||
|
||||
if (global && global.__EPISODE_CACHE__) {
|
||||
global.__EPISODE_CACHE__ = {};
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[MemoryMonitor] Could not clear global caches:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private clearImageCaches(): void {
|
||||
try {
|
||||
// Clear React Native image caches if available
|
||||
if (global && global.__IMAGE_CACHE__) {
|
||||
global.__IMAGE_CACHE__ = {};
|
||||
}
|
||||
|
||||
// Clear Expo Image cache if available
|
||||
// Note: Expo Image has its own cache management, but we can suggest cleanup
|
||||
if (global && global.expo && global.expo.ImagePicker) {
|
||||
// This is just an example - actual cache clearing would depend on the library
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[MemoryMonitor] Could not clear image caches:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually trigger memory cleanup (for external use)
|
||||
*/
|
||||
public forceCleanup(): void {
|
||||
logger.log('[MemoryMonitor] Manual cleanup triggered');
|
||||
this.performAggressiveCleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory monitoring statistics
|
||||
*/
|
||||
public getStats(): {
|
||||
lastMemoryWarning: number;
|
||||
monitoringActive: boolean;
|
||||
cleanupIntervals: {
|
||||
memoryCheck: number;
|
||||
backgroundCleanup: number;
|
||||
};
|
||||
} {
|
||||
return {
|
||||
lastMemoryWarning: this.lastMemoryWarning,
|
||||
monitoringActive: this.memoryCheckInterval !== null,
|
||||
cleanupIntervals: {
|
||||
memoryCheck: this.MEMORY_CHECK_INTERVAL,
|
||||
backgroundCleanup: this.BACKGROUND_CLEANUP_INTERVAL,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop monitoring (for cleanup when app is destroyed)
|
||||
*/
|
||||
public stopMonitoring(): void {
|
||||
if (this.appStateSubscription) {
|
||||
this.appStateSubscription.remove();
|
||||
this.appStateSubscription = null;
|
||||
}
|
||||
|
||||
if (this.memoryCheckInterval) {
|
||||
clearInterval(this.memoryCheckInterval);
|
||||
this.memoryCheckInterval = null;
|
||||
}
|
||||
|
||||
if (this.backgroundCleanupInterval) {
|
||||
clearInterval(this.backgroundCleanupInterval);
|
||||
this.backgroundCleanupInterval = null;
|
||||
}
|
||||
|
||||
logger.log('[MemoryMonitor] Stopped memory monitoring service');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle low memory warnings from the system
|
||||
*/
|
||||
public handleLowMemoryWarning(): void {
|
||||
logger.warn('[MemoryMonitor] System low memory warning received');
|
||||
this.performAggressiveCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const memoryMonitorService = MemoryMonitorService.getInstance();
|
||||
|
|
@ -7,6 +7,7 @@ import { catalogService } from './catalogService';
|
|||
import { traktService } from './traktService';
|
||||
import { tmdbService } from './tmdbService';
|
||||
import { logger } from '../utils/logger';
|
||||
import { memoryManager } from '../utils/memoryManager';
|
||||
|
||||
// Define notification storage keys
|
||||
const NOTIFICATION_STORAGE_KEY = 'stremio-notifications';
|
||||
|
|
@ -319,19 +320,37 @@ class NotificationService {
|
|||
}
|
||||
};
|
||||
|
||||
// Sync notifications for all library items
|
||||
// Sync notifications for all library items using memory-efficient batching
|
||||
private async syncNotificationsForLibrary(libraryItems: any[]): Promise<void> {
|
||||
try {
|
||||
const seriesItems = libraryItems.filter(item => item.type === 'series');
|
||||
|
||||
for (const series of seriesItems) {
|
||||
await this.updateNotificationsForSeries(series.id);
|
||||
// Longer delay to prevent overwhelming the API and reduce heating
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
// Limit series to prevent memory overflow during notifications sync
|
||||
const limitedSeries = memoryManager.limitArraySize(seriesItems, 50);
|
||||
|
||||
if (limitedSeries.length < seriesItems.length) {
|
||||
logger.warn(`[NotificationService] Limited series sync from ${seriesItems.length} to ${limitedSeries.length} to prevent memory issues`);
|
||||
}
|
||||
|
||||
// Process in small batches with memory management
|
||||
await memoryManager.processArrayInBatches(
|
||||
limitedSeries,
|
||||
async (series) => {
|
||||
try {
|
||||
await this.updateNotificationsForSeries(series.id);
|
||||
} catch (error) {
|
||||
logger.error(`[NotificationService] Failed to sync notifications for ${series.name || series.id}:`, error);
|
||||
}
|
||||
},
|
||||
3, // Very small batch size to prevent memory spikes
|
||||
800 // Longer delay to prevent API overwhelming and reduce heating
|
||||
);
|
||||
|
||||
// Force cleanup after processing
|
||||
memoryManager.forceGarbageCollection();
|
||||
|
||||
// Reduced logging verbosity
|
||||
// logger.log(`[NotificationService] Synced notifications for ${seriesItems.length} series from library`);
|
||||
// logger.log(`[NotificationService] Synced notifications for ${limitedSeries.length} series from library`);
|
||||
} catch (error) {
|
||||
logger.error('[NotificationService] Error syncing library notifications:', error);
|
||||
}
|
||||
|
|
@ -460,18 +479,31 @@ class NotificationService {
|
|||
// Reduced logging verbosity
|
||||
// logger.log(`[NotificationService] Found ${allTraktShows.size} unique Trakt shows from all sources`);
|
||||
|
||||
// Sync notifications for each Trakt show
|
||||
let syncedCount = 0;
|
||||
for (const show of allTraktShows.values()) {
|
||||
try {
|
||||
await this.updateNotificationsForSeries(show.id);
|
||||
syncedCount++;
|
||||
// Small delay to prevent API rate limiting
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
} catch (error) {
|
||||
logger.error(`[NotificationService] Failed to sync notifications for ${show.name}:`, error);
|
||||
}
|
||||
// Sync notifications for each Trakt show using memory-efficient batching
|
||||
const traktShows = Array.from(allTraktShows.values());
|
||||
const limitedTraktShows = memoryManager.limitArraySize(traktShows, 30); // Limit Trakt shows
|
||||
|
||||
if (limitedTraktShows.length < traktShows.length) {
|
||||
logger.warn(`[NotificationService] Limited Trakt shows sync from ${traktShows.length} to ${limitedTraktShows.length} to prevent memory issues`);
|
||||
}
|
||||
|
||||
let syncedCount = 0;
|
||||
await memoryManager.processArrayInBatches(
|
||||
limitedTraktShows,
|
||||
async (show) => {
|
||||
try {
|
||||
await this.updateNotificationsForSeries(show.id);
|
||||
syncedCount++;
|
||||
} catch (error) {
|
||||
logger.error(`[NotificationService] Failed to sync notifications for ${show.name}:`, error);
|
||||
}
|
||||
},
|
||||
2, // Even smaller batch size for Trakt shows
|
||||
1000 // Longer delay to prevent API rate limiting
|
||||
);
|
||||
|
||||
// Clear Trakt shows array to free memory
|
||||
memoryManager.clearObjects(traktShows, limitedTraktShows);
|
||||
|
||||
// Reduced logging verbosity
|
||||
// logger.log(`[NotificationService] Successfully synced notifications for ${syncedCount}/${allTraktShows.size} Trakt shows`);
|
||||
|
|
@ -480,31 +512,42 @@ class NotificationService {
|
|||
}
|
||||
}
|
||||
|
||||
// Enhanced series notification update with TMDB fallback
|
||||
// Enhanced series notification update with memory-efficient episode fetching
|
||||
async updateNotificationsForSeries(seriesId: string): Promise<void> {
|
||||
try {
|
||||
// Reduced logging verbosity - only log for debug purposes
|
||||
// logger.log(`[NotificationService] Updating notifications for series: ${seriesId}`);
|
||||
// Check memory pressure before processing
|
||||
memoryManager.checkMemoryPressure();
|
||||
|
||||
// Use the new memory-efficient method to fetch only upcoming episodes
|
||||
const episodeData = await stremioService.getUpcomingEpisodes('series', seriesId, {
|
||||
daysBack: 7, // 1 week back for notifications
|
||||
daysAhead: 28, // 4 weeks ahead for notifications
|
||||
maxEpisodes: 10, // Limit to 10 episodes per series for notifications
|
||||
});
|
||||
|
||||
// Try Stremio first
|
||||
let metadata = await stremioService.getMetaDetails('series', seriesId);
|
||||
let upcomingEpisodes: any[] = [];
|
||||
let metadata: any = null;
|
||||
|
||||
if (metadata && metadata.videos) {
|
||||
const now = new Date();
|
||||
const fourWeeksLater = addDays(now, 28);
|
||||
if (episodeData && episodeData.episodes.length > 0) {
|
||||
metadata = {
|
||||
name: episodeData.seriesName,
|
||||
poster: episodeData.poster,
|
||||
};
|
||||
|
||||
upcomingEpisodes = metadata.videos.filter(video => {
|
||||
if (!video.released) return false;
|
||||
const releaseDate = parseISO(video.released);
|
||||
return releaseDate > now && releaseDate < fourWeeksLater;
|
||||
}).map(video => ({
|
||||
id: video.id,
|
||||
title: (video as any).title || (video as any).name || `Episode ${video.episode}`,
|
||||
season: video.season || 0,
|
||||
episode: video.episode || 0,
|
||||
released: video.released,
|
||||
}));
|
||||
upcomingEpisodes = episodeData.episodes
|
||||
.filter(video => {
|
||||
if (!video.released) return false;
|
||||
const releaseDate = parseISO(video.released);
|
||||
const now = new Date();
|
||||
return releaseDate > now; // Only truly upcoming episodes for notifications
|
||||
})
|
||||
.map(video => ({
|
||||
id: video.id,
|
||||
title: video.title || `Episode ${video.episode}`,
|
||||
season: video.season || 0,
|
||||
episode: video.episode || 0,
|
||||
released: video.released,
|
||||
}));
|
||||
}
|
||||
|
||||
// If no upcoming episodes from Stremio, try TMDB
|
||||
|
|
@ -576,9 +619,12 @@ class NotificationService {
|
|||
notification => notification.seriesId !== seriesId
|
||||
);
|
||||
|
||||
// Schedule new notifications for upcoming episodes
|
||||
if (upcomingEpisodes.length > 0) {
|
||||
const notificationItems: NotificationItem[] = upcomingEpisodes.map(episode => ({
|
||||
// Schedule new notifications for upcoming episodes with memory limits
|
||||
if (upcomingEpisodes.length > 0 && metadata) {
|
||||
// Limit notifications per series to prevent memory overflow
|
||||
const limitedEpisodes = memoryManager.limitArraySize(upcomingEpisodes, 5);
|
||||
|
||||
const notificationItems: NotificationItem[] = limitedEpisodes.map(episode => ({
|
||||
id: episode.id,
|
||||
seriesId,
|
||||
seriesName: metadata.name,
|
||||
|
|
@ -591,13 +637,26 @@ class NotificationService {
|
|||
}));
|
||||
|
||||
const scheduledCount = await this.scheduleMultipleEpisodeNotifications(notificationItems);
|
||||
|
||||
// Clear notification items array to free memory
|
||||
memoryManager.clearObjects(notificationItems, upcomingEpisodes);
|
||||
|
||||
// Reduced logging verbosity
|
||||
// logger.log(`[NotificationService] Scheduled ${scheduledCount} notifications for ${metadata.name}`);
|
||||
} else {
|
||||
// logger.log(`[NotificationService] No upcoming episodes found for ${metadata.name}`);
|
||||
// logger.log(`[NotificationService] No upcoming episodes found for ${metadata?.name || seriesId}`);
|
||||
}
|
||||
|
||||
// Clear episode data to free memory
|
||||
if (episodeData) {
|
||||
memoryManager.clearObjects(episodeData.episodes);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`[NotificationService] Error updating notifications for series ${seriesId}:`, error);
|
||||
} finally {
|
||||
// Force cleanup after each series to prevent accumulation
|
||||
memoryManager.forceGarbageCollection();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -749,6 +749,66 @@ class StremioService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Memory-efficient method to fetch only upcoming episodes within a specific date range
|
||||
* This prevents over-fetching all episode data and reduces memory consumption
|
||||
*/
|
||||
async getUpcomingEpisodes(
|
||||
type: string,
|
||||
id: string,
|
||||
options: {
|
||||
daysBack?: number;
|
||||
daysAhead?: number;
|
||||
maxEpisodes?: number;
|
||||
preferredAddonId?: string;
|
||||
} = {}
|
||||
): Promise<{ seriesName: string; poster: string; episodes: any[] } | null> {
|
||||
const { daysBack = 14, daysAhead = 28, maxEpisodes = 50, preferredAddonId } = options;
|
||||
|
||||
try {
|
||||
// Get metadata first (this is lightweight compared to episodes)
|
||||
const metadata = await this.getMetaDetails(type, id, preferredAddonId);
|
||||
if (!metadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If no videos array exists, return basic info
|
||||
if (!metadata.videos || metadata.videos.length === 0) {
|
||||
return {
|
||||
seriesName: metadata.name,
|
||||
poster: metadata.poster || '',
|
||||
episodes: []
|
||||
};
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const startDate = new Date(now.getTime() - (daysBack * 24 * 60 * 60 * 1000));
|
||||
const endDate = new Date(now.getTime() + (daysAhead * 24 * 60 * 60 * 1000));
|
||||
|
||||
// Filter episodes to only include those within our date range
|
||||
// This is done immediately after fetching to reduce memory footprint
|
||||
const filteredEpisodes = metadata.videos
|
||||
.filter(video => {
|
||||
if (!video.released) return false;
|
||||
const releaseDate = new Date(video.released);
|
||||
return releaseDate >= startDate && releaseDate <= endDate;
|
||||
})
|
||||
.sort((a, b) => new Date(a.released).getTime() - new Date(b.released).getTime())
|
||||
.slice(0, maxEpisodes); // Limit number of episodes to prevent memory overflow
|
||||
|
||||
logger.log(`[StremioService] Filtered ${metadata.videos.length} episodes down to ${filteredEpisodes.length} upcoming episodes for ${metadata.name}`);
|
||||
|
||||
return {
|
||||
seriesName: metadata.name,
|
||||
poster: metadata.poster || '',
|
||||
episodes: filteredEpisodes
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`[StremioService] Error fetching upcoming episodes for ${id}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Modify getStreams to use this.getInstalledAddons() instead of getEnabledAddons
|
||||
async getStreams(type: string, id: string, callback?: StreamCallback): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
|
|
|||
260
src/utils/memoryManager.ts
Normal file
260
src/utils/memoryManager.ts
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
import { logger } from './logger';
|
||||
|
||||
/**
|
||||
* Memory management utilities to help prevent OutOfMemoryError
|
||||
* These utilities help manage JavaScript heap usage and optimize garbage collection
|
||||
*/
|
||||
export class MemoryManager {
|
||||
private static instance: MemoryManager;
|
||||
private lastCleanup: number = 0;
|
||||
private readonly CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
private memoryWarningThreshold = 200 * 1024 * 1024; // 200MB warning threshold
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): MemoryManager {
|
||||
if (!MemoryManager.instance) {
|
||||
MemoryManager.instance = new MemoryManager();
|
||||
}
|
||||
return MemoryManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force garbage collection (React Native specific)
|
||||
* This suggests to the JavaScript engine to run garbage collection
|
||||
*/
|
||||
public forceGarbageCollection(): void {
|
||||
try {
|
||||
// Request garbage collection if available (development builds)
|
||||
if (global && typeof global.gc === 'function') {
|
||||
global.gc();
|
||||
logger.log('[MemoryManager] Forced garbage collection');
|
||||
} else if (__DEV__) {
|
||||
// In development, we can try to trigger GC by creating and releasing large objects
|
||||
this.triggerGCInDev();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[MemoryManager] Could not force garbage collection:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Development-only method to trigger garbage collection
|
||||
*/
|
||||
private triggerGCInDev(): void {
|
||||
try {
|
||||
// Create a large temporary object to trigger GC
|
||||
const largeArray = new Array(1000000).fill(null);
|
||||
// Release reference
|
||||
largeArray.length = 0;
|
||||
} catch (error) {
|
||||
// Ignore errors in GC triggering
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear large objects from memory by setting them to null
|
||||
*/
|
||||
public clearObjects(...objects: any[]): void {
|
||||
objects.forEach((obj, index) => {
|
||||
if (obj && typeof obj === 'object') {
|
||||
// Clear arrays
|
||||
if (Array.isArray(obj)) {
|
||||
obj.length = 0;
|
||||
}
|
||||
// Clear object properties
|
||||
else {
|
||||
Object.keys(obj).forEach(key => {
|
||||
try {
|
||||
delete obj[key];
|
||||
} catch (error) {
|
||||
// Some properties might not be deletable
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimized array processing to prevent memory accumulation
|
||||
* Processes arrays in batches to allow garbage collection between batches
|
||||
*/
|
||||
public async processArrayInBatches<T, R>(
|
||||
array: T[],
|
||||
processor: (item: T, index: number) => Promise<R> | R,
|
||||
batchSize: number = 10,
|
||||
delayMs: number = 0
|
||||
): Promise<R[]> {
|
||||
const results: R[] = [];
|
||||
|
||||
for (let i = 0; i < array.length; i += batchSize) {
|
||||
const batch = array.slice(i, i + batchSize);
|
||||
|
||||
// Process batch
|
||||
const batchResults = await Promise.all(
|
||||
batch.map((item, batchIndex) => processor(item, i + batchIndex))
|
||||
);
|
||||
|
||||
results.push(...batchResults);
|
||||
|
||||
// Force cleanup between batches for large datasets
|
||||
if (i > 0 && i % (batchSize * 5) === 0) {
|
||||
this.forceGarbageCollection();
|
||||
}
|
||||
|
||||
// Optional delay to prevent blocking
|
||||
if (delayMs > 0 && i + batchSize < array.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor memory usage and trigger cleanup if needed
|
||||
*/
|
||||
public checkMemoryPressure(): boolean {
|
||||
const now = Date.now();
|
||||
const timeSinceLastCleanup = now - this.lastCleanup;
|
||||
|
||||
// Perform cleanup if enough time has passed
|
||||
if (timeSinceLastCleanup >= this.CLEANUP_INTERVAL) {
|
||||
this.performMemoryCleanup();
|
||||
this.lastCleanup = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform memory cleanup operations
|
||||
*/
|
||||
private performMemoryCleanup(): void {
|
||||
try {
|
||||
logger.log('[MemoryManager] Performing memory cleanup');
|
||||
|
||||
// Force garbage collection
|
||||
this.forceGarbageCollection();
|
||||
|
||||
// Clear any global caches if they exist
|
||||
this.clearGlobalCaches();
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[MemoryManager] Error during memory cleanup:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear global caches to free memory
|
||||
*/
|
||||
private clearGlobalCaches(): void {
|
||||
try {
|
||||
// Clear any image caches (React Native specific)
|
||||
if (global && (global as any).__IMAGE_CACHE__) {
|
||||
(global as any).__IMAGE_CACHE__ = {};
|
||||
}
|
||||
|
||||
// Clear any other global caches your app might have
|
||||
if (global && (global as any).__APP_CACHE__) {
|
||||
(global as any).__APP_CACHE__ = {};
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[MemoryManager] Could not clear global caches:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a memory-efficient filter for large arrays
|
||||
* Processes items one by one and yields to the event loop periodically
|
||||
*/
|
||||
public async filterLargeArray<T>(
|
||||
array: T[],
|
||||
predicate: (item: T, index: number) => boolean,
|
||||
yieldEvery: number = 100
|
||||
): Promise<T[]> {
|
||||
const result: T[] = [];
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (predicate(array[i], i)) {
|
||||
result.push(array[i]);
|
||||
}
|
||||
|
||||
// Yield to event loop periodically to prevent blocking
|
||||
if (i > 0 && i % yieldEvery === 0) {
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a memory-efficient map for large arrays
|
||||
* Processes items in batches and manages memory between batches
|
||||
*/
|
||||
public async mapLargeArray<T, R>(
|
||||
array: T[],
|
||||
mapper: (item: T, index: number) => R,
|
||||
batchSize: number = 50
|
||||
): Promise<R[]> {
|
||||
const results: R[] = [];
|
||||
|
||||
for (let i = 0; i < array.length; i += batchSize) {
|
||||
const batch = array.slice(i, i + batchSize);
|
||||
const batchResults = batch.map((item, batchIndex) => mapper(item, i + batchIndex));
|
||||
|
||||
results.push(...batchResults);
|
||||
|
||||
// Cleanup between large batches
|
||||
if (i > 0 && i % (batchSize * 10) === 0) {
|
||||
this.forceGarbageCollection();
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit array size to prevent memory overflow
|
||||
*/
|
||||
public limitArraySize<T>(array: T[], maxSize: number): T[] {
|
||||
if (array.length <= maxSize) {
|
||||
return array;
|
||||
}
|
||||
|
||||
logger.warn(`[MemoryManager] Array size (${array.length}) exceeds limit (${maxSize}), truncating`);
|
||||
return array.slice(0, maxSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep clone with memory optimization
|
||||
* Only clones necessary properties to reduce memory footprint
|
||||
*/
|
||||
public optimizedClone<T>(obj: T, maxDepth: number = 3, currentDepth: number = 0): T {
|
||||
if (currentDepth >= maxDepth || obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item =>
|
||||
this.optimizedClone(item, maxDepth, currentDepth + 1)
|
||||
) as unknown as T;
|
||||
}
|
||||
|
||||
const cloned = {} as T;
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
cloned[key] = this.optimizedClone(obj[key], maxDepth, currentDepth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return cloned;
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const memoryManager = MemoryManager.getInstance();
|
||||
Loading…
Reference in a new issue