performance:

- Performance: `xc_get_epg` now uses `select_related('epg_data__epg_source')` on all three channel fetch paths. Previously each request triggered 2 additional queries to resolve `channel.epg_data` and `channel.epg_data.epg_source`.
- Performance: `generate_m3u` now uses `prefetch_related` for streams when `?direct=true` is requested, eliminating N+1 stream queries (one per channel) on that code path.
- Performance: `EPGGridAPIView` (`apps/epg/api_views.py`) now uses `select_related('epg_data__epg_source')` on the `channels_with_custom_dummy` queryset, eliminating 2 extra queries per channel (for `epg_data` and `epg_source`) in the dummy EPG generation loop.
This commit is contained in:
SergeantPanda 2026-04-16 13:19:19 -05:00
parent 16984bc386
commit 54ab3e8981
3 changed files with 19 additions and 10 deletions

View file

@ -73,6 +73,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Performance: `generate_m3u`, `generate_epg`, and `xc_get_live_streams` now use `select_related('channel_group', 'logo')` (or `select_related('logo')` for EPG) on every Channel queryset in `apps/output/views.py`. Previously each channel in the loop triggered a separate database query for its `logo` and `channel_group` foreign keys; with the JOIN-based prefetch this is reduced to a single query per request. On deployments with ~2 000 channels, `xc_get_live_streams` response time drops from ~2.54 s to ~250450 ms. (Closes #1127) — Thanks [@xBOBxSAGETx](https://github.com/xBOBxSAGETx)
- Performance: `generate_epg` now uses `select_related('epg_data__epg_source')` on all EPG channel querysets, eliminating N+1 database queries for `EPGSource` traversal per channel (~15 s improvement on ~2000-channel deployments; total EPG generation time dropped from ~87 s to ~72 s in benchmarks).
- Performance: `xc_get_epg` now uses `select_related('epg_data__epg_source')` on all three channel fetch paths. Previously each request triggered 2 additional queries to resolve `channel.epg_data` and `channel.epg_data.epg_source`.
- Performance: `generate_m3u` now uses `prefetch_related` for streams when `?direct=true` is requested, eliminating N+1 stream queries (one per channel) on that code path.
- Performance: `EPGGridAPIView` (`apps/epg/api_views.py`) now uses `select_related('epg_data__epg_source')` on the `channels_with_custom_dummy` queryset, eliminating 2 extra queries per channel (for `epg_data` and `epg_source`) in the dummy EPG generation loop.
- Performance: `generate_epg` now issues a single cross-channel `ProgramData` bulk query. `.values()` returns plain dicts, bypassing per-row Django model instantiation. Results are consumed in independent 5000-row keyset-paginated chunks. Combined with the `select_related` improvements above, EPG generation time on large deployments is significantly reduced.
- Performance: `xc_get_live_streams` no longer calls `ChannelGroup.objects.get_or_create(name="Default Group")` once per null-group channel; replaced with a lazy-initialised closure that executes at most one query regardless of how many ungrouped channels are present.
- AIO containers now connect to the internal PostgreSQL instance via a Unix domain socket instead of TCP loopback. Users who have `POSTGRES_HOST` explicitly set to `localhost` or `127.0.0.1` in their compose file are automatically migrated to the socket path; any other explicit value (external host/IP) is left untouched. — Thanks [@JCBird1012](https://github.com/JCBird1012)

View file

@ -175,7 +175,7 @@ class EPGGridAPIView(APIView):
# Get channels with custom dummy EPG sources (generate on-demand with patterns)
channels_with_custom_dummy = Channel.objects.filter(
epg_data__epg_source__source_type='dummy'
).distinct()
).select_related('epg_data__epg_source').distinct()
# Log what we found
without_count = channels_without_epg.count()

View file

@ -1,8 +1,8 @@
import ipaddress
from django.http import HttpResponse, JsonResponse, Http404, HttpResponseForbidden, StreamingHttpResponse
from rest_framework.response import Response
from django.urls import reverse
from apps.channels.models import Channel, ChannelProfile, ChannelGroup
from apps.channels.models import Channel, ChannelProfile, ChannelGroup, Stream
from django.db.models import Prefetch
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from apps.epg.models import ProgramData
@ -11,9 +11,8 @@ from dispatcharr.utils import network_access_allowed
from django.utils import timezone as django_timezone
from django.shortcuts import get_object_or_404
from datetime import datetime, timedelta
import html # Add this import for XML escaping
import json # Add this import for JSON parsing
import time # Add this import for keep-alive delays
import html
import time
from tzlocal import get_localzone
from urllib.parse import urlparse
import base64
@ -175,6 +174,12 @@ def generate_m3u(request, profile_name=None, user=None):
# Check if direct stream URLs should be used instead of proxy
use_direct_urls = request.GET.get('direct', 'false').lower() == 'true'
# Prefetch streams only when direct URLs are requested (avoids N+1 per channel)
if use_direct_urls:
channels = channels.prefetch_related(
Prefetch('streams', queryset=Stream.objects.order_by('channelstream__order'))
)
# Get the source to use for tvg-id value
# Options: 'channel_number' (default), 'tvg_id', 'gracenote'
tvg_id_source = request.GET.get('tvg_id_source', 'channel_number').lower()
@ -262,7 +267,8 @@ def generate_m3u(request, profile_name=None, user=None):
stream_url = f"{base_url}/live/{xc_username}/{xc_password}/{channel.id}"
elif use_direct_urls:
# Try to get the first stream's direct URL
first_stream = channel.streams.order_by('channelstream__order').first()
all_streams = channel.streams.all()
first_stream = all_streams[0] if all_streams else None
if first_stream and first_stream.url:
# Use the direct stream URL
stream_url = first_stream.url
@ -2253,7 +2259,7 @@ def xc_get_epg(request, user, short=False):
# Hide adult content if user preference is set
if (user.custom_properties or {}).get('hide_adult_content', False):
filters["is_adult"] = False
channel = Channel.objects.filter(**filters).first()
channel = Channel.objects.filter(**filters).select_related('epg_data__epg_source').first()
else:
# User has specific limited profiles assigned
filters = {
@ -2265,12 +2271,12 @@ def xc_get_epg(request, user, short=False):
# Hide adult content if user preference is set
if (user.custom_properties or {}).get('hide_adult_content', False):
filters["is_adult"] = False
channel = Channel.objects.filter(**filters).distinct().first()
channel = Channel.objects.filter(**filters).select_related('epg_data__epg_source').distinct().first()
if not channel:
raise Http404()
else:
channel = get_object_or_404(Channel, id=channel_id)
channel = get_object_or_404(Channel.objects.select_related('epg_data__epg_source'), id=channel_id)
if not channel:
raise Http404()