mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-28 20:03:34 +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 AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import * as Sentry from '@sentry/react-native';
|
import * as Sentry from '@sentry/react-native';
|
||||||
import UpdateService from './src/services/updateService';
|
import UpdateService from './src/services/updateService';
|
||||||
|
import { memoryMonitorService } from './src/services/memoryMonitorService';
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: 'https://1a58bf436454d346e5852b7bfd3c95e8@o4509536317276160.ingest.de.sentry.io/4509536317734992',
|
dsn: 'https://1a58bf436454d346e5852b7bfd3c95e8@o4509536317276160.ingest.de.sentry.io/4509536317734992',
|
||||||
|
|
@ -81,7 +82,7 @@ const ThemedApp = () => {
|
||||||
handleDismiss,
|
handleDismiss,
|
||||||
} = useUpdatePopup();
|
} = useUpdatePopup();
|
||||||
|
|
||||||
// Check onboarding status and initialize update service
|
// Check onboarding status and initialize services
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeApp = async () => {
|
const initializeApp = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -91,6 +92,11 @@ const ThemedApp = () => {
|
||||||
|
|
||||||
// Initialize update service
|
// Initialize update service
|
||||||
await UpdateService.initialize();
|
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) {
|
} catch (error) {
|
||||||
console.error('Error initializing app:', error);
|
console.error('Error initializing app:', error);
|
||||||
// Default to showing onboarding if we can't check
|
// 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 { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { useTraktContext } from '../../contexts/TraktContext';
|
import { useTraktContext } from '../../contexts/TraktContext';
|
||||||
import { stremioService } from '../../services/stremioService';
|
|
||||||
import { tmdbService } from '../../services/tmdbService';
|
|
||||||
import { useLibrary } from '../../hooks/useLibrary';
|
import { useLibrary } from '../../hooks/useLibrary';
|
||||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||||
import { parseISO, isThisWeek, format, isAfter, isBefore } from 'date-fns';
|
import { parseISO, isThisWeek, format, isAfter, isBefore } from 'date-fns';
|
||||||
import Animated, { FadeIn } from 'react-native-reanimated';
|
import Animated, { FadeIn } from 'react-native-reanimated';
|
||||||
import { useCalendarData } from '../../hooks/useCalendarData';
|
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
|
// Compute base sizes; actual tablet sizes will be adjusted inside component for responsiveness
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
|
|
@ -46,15 +46,6 @@ interface ThisWeekEpisode {
|
||||||
|
|
||||||
export const ThisWeekSection = React.memo(() => {
|
export const ThisWeekSection = React.memo(() => {
|
||||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const { libraryItems, loading: libraryLoading } = useLibrary();
|
|
||||||
const {
|
|
||||||
isAuthenticated: traktAuthenticated,
|
|
||||||
isLoading: traktLoading,
|
|
||||||
watchedShows,
|
|
||||||
watchlistShows,
|
|
||||||
continueWatching,
|
|
||||||
loadAllCollections
|
|
||||||
} = useTraktContext();
|
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const { calendarData, loading } = useCalendarData();
|
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 computedItemWidth = useMemo(() => (isTablet ? Math.min(deviceWidth * 0.46, 560) : ITEM_WIDTH), [isTablet, deviceWidth]);
|
||||||
const computedItemHeight = useMemo(() => (isTablet ? 220 : ITEM_HEIGHT), [isTablet]);
|
const computedItemHeight = useMemo(() => (isTablet ? 220 : ITEM_HEIGHT), [isTablet]);
|
||||||
|
|
||||||
|
// Use the already memory-optimized calendar data instead of fetching separately
|
||||||
const thisWeekEpisodes = useMemo(() => {
|
const thisWeekEpisodes = useMemo(() => {
|
||||||
const thisWeekSection = calendarData.find(section => section.title === 'This Week');
|
const thisWeekSection = calendarData.find(section => section.title === 'This Week');
|
||||||
if (!thisWeekSection) return [];
|
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,
|
...episode,
|
||||||
isReleased: isBefore(parseISO(episode.releaseDate), new Date()),
|
isReleased: episode.releaseDate ? isBefore(parseISO(episode.releaseDate), new Date()) : false,
|
||||||
}));
|
}));
|
||||||
}, [calendarData]);
|
}, [calendarData]);
|
||||||
|
|
||||||
|
|
@ -104,8 +99,9 @@ export const ThisWeekSection = React.memo(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderEpisodeItem = ({ item, index }: { item: ThisWeekEpisode, index: number }) => {
|
const renderEpisodeItem = ({ item, index }: { item: ThisWeekEpisode, index: number }) => {
|
||||||
const releaseDate = parseISO(item.releaseDate);
|
// Handle episodes without release dates gracefully
|
||||||
const formattedDate = format(releaseDate, 'E, MMM d');
|
const releaseDate = item.releaseDate ? parseISO(item.releaseDate) : null;
|
||||||
|
const formattedDate = releaseDate ? format(releaseDate, 'E, MMM d') : 'TBA';
|
||||||
const isReleased = item.isReleased;
|
const isReleased = item.isReleased;
|
||||||
|
|
||||||
// Use episode still image if available, fallback to series poster
|
// 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 { stremioService } from '../services/stremioService';
|
||||||
import { tmdbService } from '../services/tmdbService';
|
import { tmdbService } from '../services/tmdbService';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
import { memoryManager } from '../utils/memoryManager';
|
||||||
import { parseISO, isBefore, isAfter, startOfToday, addWeeks, isThisWeek } from 'date-fns';
|
import { parseISO, isBefore, isAfter, startOfToday, addWeeks, isThisWeek } from 'date-fns';
|
||||||
import { StreamingContent } from '../services/catalogService';
|
import { StreamingContent } from '../services/catalogService';
|
||||||
|
|
||||||
|
|
@ -53,6 +54,9 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check memory pressure and cleanup if needed
|
||||||
|
memoryManager.checkMemoryPressure();
|
||||||
|
|
||||||
if (!forceRefresh) {
|
if (!forceRefresh) {
|
||||||
const cachedData = await robustCalendarCache.getCachedCalendarData(
|
const cachedData = await robustCalendarCache.getCachedCalendarData(
|
||||||
libraryItems,
|
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})`);
|
logger.log(`[CalendarData] Total series to check: ${allSeries.length} (Library: ${librarySeries.length}, Trakt: ${allSeries.length - librarySeries.length})`);
|
||||||
|
|
||||||
let allEpisodes: CalendarEpisode[] = [];
|
let allEpisodes: CalendarEpisode[] = [];
|
||||||
let seriesWithoutEpisodes: CalendarEpisode[] = [];
|
let seriesWithoutEpisodes: CalendarEpisode[] = [];
|
||||||
|
|
||||||
for (const series of allSeries) {
|
// Process series in memory-efficient batches to prevent OOM
|
||||||
|
const processedSeries = await memoryManager.processArrayInBatches(
|
||||||
|
allSeries,
|
||||||
|
async (series: StreamingContent, index: number) => {
|
||||||
try {
|
try {
|
||||||
const metadata = await stremioService.getMetaDetails(series.type, series.id);
|
// Use the new memory-efficient method to fetch only upcoming episodes
|
||||||
|
const episodeData = await stremioService.getUpcomingEpisodes(series.type, series.id, {
|
||||||
if (metadata?.videos && metadata.videos.length > 0) {
|
daysBack: 14, // 2 weeks back
|
||||||
const today = startOfToday();
|
daysAhead: 28, // 4 weeks ahead
|
||||||
const fourWeeksLater = addWeeks(today, 4);
|
maxEpisodes: 25, // Limit episodes per series
|
||||||
const twoWeeksAgo = addWeeks(today, -2);
|
});
|
||||||
|
|
||||||
|
if (episodeData && episodeData.episodes.length > 0) {
|
||||||
const tmdbId = await tmdbService.findTMDBIdByIMDB(series.id);
|
const tmdbId = await tmdbService.findTMDBIdByIMDB(series.id);
|
||||||
let tmdbEpisodes: { [key: string]: any } = {};
|
let tmdbEpisodes: { [key: string]: any } = {};
|
||||||
|
|
||||||
if (tmdbId) {
|
// Only fetch TMDB data if we need it and limit it
|
||||||
const allTMDBEpisodes = await tmdbService.getAllEpisodes(tmdbId);
|
if (tmdbId && episodeData.episodes.length > 0) {
|
||||||
Object.values(allTMDBEpisodes).forEach(seasonEpisodes => {
|
try {
|
||||||
seasonEpisodes.forEach(episode => {
|
// 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}`;
|
const key = `${episode.season_number}:${episode.episode_number}`;
|
||||||
tmdbEpisodes[key] = episode;
|
tmdbEpisodes[key] = episode;
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
} catch (tmdbError) {
|
||||||
|
logger.warn(`[CalendarData] TMDB fetch failed for ${series.name}, continuing without additional metadata`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const upcomingEpisodes = metadata.videos
|
// Transform episodes with memory-efficient processing
|
||||||
.filter(video => {
|
const transformedEpisodes = episodeData.episodes.map(video => {
|
||||||
if (!video.released) return false;
|
|
||||||
const releaseDate = parseISO(video.released);
|
|
||||||
return isBefore(releaseDate, fourWeeksLater) && isAfter(releaseDate, twoWeeksAgo);
|
|
||||||
})
|
|
||||||
.map(video => {
|
|
||||||
const tmdbEpisode = tmdbEpisodes[`${video.season}:${video.episode}`] || {};
|
const tmdbEpisode = tmdbEpisodes[`${video.season}:${video.episode}`] || {};
|
||||||
return {
|
return {
|
||||||
id: video.id,
|
id: video.id,
|
||||||
seriesId: series.id,
|
seriesId: series.id,
|
||||||
title: tmdbEpisode.name || video.title || `Episode ${video.episode}`,
|
title: tmdbEpisode.name || video.title || `Episode ${video.episode}`,
|
||||||
seriesName: series.name || metadata.name,
|
seriesName: series.name || episodeData.seriesName,
|
||||||
poster: series.poster || metadata.poster || '',
|
poster: series.poster || episodeData.poster || '',
|
||||||
releaseDate: video.released,
|
releaseDate: video.released,
|
||||||
season: video.season || 0,
|
season: video.season || 0,
|
||||||
episode: video.episode || 0,
|
episode: video.episode || 0,
|
||||||
|
|
@ -189,24 +210,88 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (upcomingEpisodes.length > 0) {
|
// Clear references to help garbage collection
|
||||||
allEpisodes = [...allEpisodes, ...upcomingEpisodes];
|
memoryManager.clearObjects(tmdbEpisodes);
|
||||||
|
|
||||||
|
return { type: 'episodes', data: transformedEpisodes };
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
logger.error(`Error fetching episodes for ${series.name}:`, 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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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());
|
allEpisodes.sort((a, b) => new Date(a.releaseDate).getTime() - new Date(b.releaseDate).getTime());
|
||||||
|
|
||||||
const thisWeekEpisodes = allEpisodes.filter(ep => isThisWeek(parseISO(ep.releaseDate)));
|
// Use memory-efficient filtering
|
||||||
const upcomingEpisodes = allEpisodes.filter(ep => isAfter(parseISO(ep.releaseDate), new Date()) && !isThisWeek(parseISO(ep.releaseDate)));
|
const thisWeekEpisodes = await memoryManager.filterLargeArray(
|
||||||
const recentEpisodes = allEpisodes.filter(ep => isBefore(parseISO(ep.releaseDate), new Date()) && !isThisWeek(parseISO(ep.releaseDate)));
|
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[] = [];
|
const sections: CalendarSection[] = [];
|
||||||
if (thisWeekEpisodes.length > 0) sections.push({ title: 'This Week', data: thisWeekEpisodes });
|
if (thisWeekEpisodes.length > 0) sections.push({ title: 'This Week', data: thisWeekEpisodes });
|
||||||
|
|
@ -216,6 +301,9 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
||||||
|
|
||||||
setCalendarData(sections);
|
setCalendarData(sections);
|
||||||
|
|
||||||
|
// Clear large arrays to help garbage collection
|
||||||
|
memoryManager.clearObjects(allEpisodes, thisWeekEpisodes, upcomingEpisodes, recentEpisodes);
|
||||||
|
|
||||||
await robustCalendarCache.setCachedCalendarData(
|
await robustCalendarCache.setCachedCalendarData(
|
||||||
sections,
|
sections,
|
||||||
libraryItems,
|
libraryItems,
|
||||||
|
|
@ -223,7 +311,7 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
||||||
);
|
);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error fetching calendar data:', error);
|
logger.error('[CalendarData] Error fetching calendar data:', error);
|
||||||
await robustCalendarCache.setCachedCalendarData(
|
await robustCalendarCache.setCachedCalendarData(
|
||||||
[],
|
[],
|
||||||
libraryItems,
|
libraryItems,
|
||||||
|
|
@ -231,6 +319,8 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
// Force garbage collection after processing
|
||||||
|
memoryManager.forceGarbageCollection();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [libraryItems, traktAuthenticated, watchlistShows, continueWatching, watchedShows]);
|
}, [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 { CalendarSection as CalendarSectionComponent } from '../components/calendar/CalendarSection';
|
||||||
import { tmdbService } from '../services/tmdbService';
|
import { tmdbService } from '../services/tmdbService';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
import { memoryManager } from '../utils/memoryManager';
|
||||||
import { useCalendarData } from '../hooks/useCalendarData';
|
import { useCalendarData } from '../hooks/useCalendarData';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
|
|
@ -73,6 +74,8 @@ const CalendarScreen = () => {
|
||||||
|
|
||||||
const onRefresh = useCallback(() => {
|
const onRefresh = useCallback(() => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
|
// Check memory pressure before refresh
|
||||||
|
memoryManager.checkMemoryPressure();
|
||||||
refresh(true);
|
refresh(true);
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
@ -206,10 +209,15 @@ const CalendarScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Process all episodes once data is loaded
|
// Process all episodes once data is loaded - using memory-efficient approach
|
||||||
const allEpisodes = calendarData.reduce((acc: CalendarEpisode[], section: CalendarSection) =>
|
const allEpisodes = React.useMemo(() => {
|
||||||
|
const episodes = calendarData.reduce((acc: CalendarEpisode[], section: CalendarSection) =>
|
||||||
[...acc, ...section.data], [] as CalendarEpisode[]);
|
[...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
|
// Log when rendering with relevant state info
|
||||||
logger.log(`[Calendar] Rendering: loading=${loading}, calendarData sections=${calendarData.length}, allEpisodes=${allEpisodes.length}`);
|
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 { traktService } from './traktService';
|
||||||
import { tmdbService } from './tmdbService';
|
import { tmdbService } from './tmdbService';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
import { memoryManager } from '../utils/memoryManager';
|
||||||
|
|
||||||
// Define notification storage keys
|
// Define notification storage keys
|
||||||
const NOTIFICATION_STORAGE_KEY = 'stremio-notifications';
|
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> {
|
private async syncNotificationsForLibrary(libraryItems: any[]): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const seriesItems = libraryItems.filter(item => item.type === 'series');
|
const seriesItems = libraryItems.filter(item => item.type === 'series');
|
||||||
|
|
||||||
for (const series of seriesItems) {
|
// Limit series to prevent memory overflow during notifications sync
|
||||||
await this.updateNotificationsForSeries(series.id);
|
const limitedSeries = memoryManager.limitArraySize(seriesItems, 50);
|
||||||
// Longer delay to prevent overwhelming the API and reduce heating
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
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
|
// 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) {
|
} catch (error) {
|
||||||
logger.error('[NotificationService] Error syncing library notifications:', error);
|
logger.error('[NotificationService] Error syncing library notifications:', error);
|
||||||
}
|
}
|
||||||
|
|
@ -460,18 +479,31 @@ class NotificationService {
|
||||||
// Reduced logging verbosity
|
// Reduced logging verbosity
|
||||||
// logger.log(`[NotificationService] Found ${allTraktShows.size} unique Trakt shows from all sources`);
|
// logger.log(`[NotificationService] Found ${allTraktShows.size} unique Trakt shows from all sources`);
|
||||||
|
|
||||||
// Sync notifications for each Trakt show
|
// 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;
|
let syncedCount = 0;
|
||||||
for (const show of allTraktShows.values()) {
|
await memoryManager.processArrayInBatches(
|
||||||
|
limitedTraktShows,
|
||||||
|
async (show) => {
|
||||||
try {
|
try {
|
||||||
await this.updateNotificationsForSeries(show.id);
|
await this.updateNotificationsForSeries(show.id);
|
||||||
syncedCount++;
|
syncedCount++;
|
||||||
// Small delay to prevent API rate limiting
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[NotificationService] Failed to sync notifications for ${show.name}:`, 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
|
// Reduced logging verbosity
|
||||||
// logger.log(`[NotificationService] Successfully synced notifications for ${syncedCount}/${allTraktShows.size} Trakt shows`);
|
// logger.log(`[NotificationService] Successfully synced notifications for ${syncedCount}/${allTraktShows.size} Trakt shows`);
|
||||||
|
|
@ -480,27 +512,38 @@ class NotificationService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced series notification update with TMDB fallback
|
// Enhanced series notification update with memory-efficient episode fetching
|
||||||
async updateNotificationsForSeries(seriesId: string): Promise<void> {
|
async updateNotificationsForSeries(seriesId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Reduced logging verbosity - only log for debug purposes
|
// Check memory pressure before processing
|
||||||
// logger.log(`[NotificationService] Updating notifications for series: ${seriesId}`);
|
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 upcomingEpisodes: any[] = [];
|
||||||
|
let metadata: any = null;
|
||||||
|
|
||||||
if (metadata && metadata.videos) {
|
if (episodeData && episodeData.episodes.length > 0) {
|
||||||
const now = new Date();
|
metadata = {
|
||||||
const fourWeeksLater = addDays(now, 28);
|
name: episodeData.seriesName,
|
||||||
|
poster: episodeData.poster,
|
||||||
|
};
|
||||||
|
|
||||||
upcomingEpisodes = metadata.videos.filter(video => {
|
upcomingEpisodes = episodeData.episodes
|
||||||
|
.filter(video => {
|
||||||
if (!video.released) return false;
|
if (!video.released) return false;
|
||||||
const releaseDate = parseISO(video.released);
|
const releaseDate = parseISO(video.released);
|
||||||
return releaseDate > now && releaseDate < fourWeeksLater;
|
const now = new Date();
|
||||||
}).map(video => ({
|
return releaseDate > now; // Only truly upcoming episodes for notifications
|
||||||
|
})
|
||||||
|
.map(video => ({
|
||||||
id: video.id,
|
id: video.id,
|
||||||
title: (video as any).title || (video as any).name || `Episode ${video.episode}`,
|
title: video.title || `Episode ${video.episode}`,
|
||||||
season: video.season || 0,
|
season: video.season || 0,
|
||||||
episode: video.episode || 0,
|
episode: video.episode || 0,
|
||||||
released: video.released,
|
released: video.released,
|
||||||
|
|
@ -576,9 +619,12 @@ class NotificationService {
|
||||||
notification => notification.seriesId !== seriesId
|
notification => notification.seriesId !== seriesId
|
||||||
);
|
);
|
||||||
|
|
||||||
// Schedule new notifications for upcoming episodes
|
// Schedule new notifications for upcoming episodes with memory limits
|
||||||
if (upcomingEpisodes.length > 0) {
|
if (upcomingEpisodes.length > 0 && metadata) {
|
||||||
const notificationItems: NotificationItem[] = upcomingEpisodes.map(episode => ({
|
// Limit notifications per series to prevent memory overflow
|
||||||
|
const limitedEpisodes = memoryManager.limitArraySize(upcomingEpisodes, 5);
|
||||||
|
|
||||||
|
const notificationItems: NotificationItem[] = limitedEpisodes.map(episode => ({
|
||||||
id: episode.id,
|
id: episode.id,
|
||||||
seriesId,
|
seriesId,
|
||||||
seriesName: metadata.name,
|
seriesName: metadata.name,
|
||||||
|
|
@ -591,13 +637,26 @@ class NotificationService {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const scheduledCount = await this.scheduleMultipleEpisodeNotifications(notificationItems);
|
const scheduledCount = await this.scheduleMultipleEpisodeNotifications(notificationItems);
|
||||||
|
|
||||||
|
// Clear notification items array to free memory
|
||||||
|
memoryManager.clearObjects(notificationItems, upcomingEpisodes);
|
||||||
|
|
||||||
// Reduced logging verbosity
|
// Reduced logging verbosity
|
||||||
// logger.log(`[NotificationService] Scheduled ${scheduledCount} notifications for ${metadata.name}`);
|
// logger.log(`[NotificationService] Scheduled ${scheduledCount} notifications for ${metadata.name}`);
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
logger.error(`[NotificationService] Error updating notifications for series ${seriesId}:`, 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
|
// Modify getStreams to use this.getInstalledAddons() instead of getEnabledAddons
|
||||||
async getStreams(type: string, id: string, callback?: StreamCallback): Promise<void> {
|
async getStreams(type: string, id: string, callback?: StreamCallback): Promise<void> {
|
||||||
await this.ensureInitialized();
|
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