Files
offline-db/content/tests/test_anime_detail_performance.py
Barış Keserandgoogle-labs-jules[bot] c3a3a9b716 Optimize Anime Detail view N+1 query and fix serializers (#162)
- Add `seasons__episodes__video_files` and `seasons__episodes__external_sources` to `prefetch_related` in `AnimeViewSet`.
- Reduce queries for Anime Detail from ~25 to ~7 (1 query per relation instead of 1 per episode).
- Fix `content/serializers.py` to match current models using `source` mapping to preserve API contract (e.g. `file_url` -> `hls_path`).
- Add regression test `content/tests/test_anime_detail_performance.py`.
- Add missing `djangorestframework-simplejwt` dependency to `requirements.txt`.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-02-24 12:42:39 +00:00

84 lines
3.9 KiB
Python

from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework import status
from django.test.utils import CaptureQueriesContext
from django.db import connection
from content.models import Anime, Season, Episode, VideoFile, ExternalSource, Genre
class AnimeDetailPerformanceTest(APITestCase):
def setUp(self):
self.genre = Genre.objects.create(name="Action", slug="action")
self.anime = Anime.objects.create(title="Performance Test Anime")
self.anime.genres.add(self.genre)
# Create 2 seasons
for s in range(1, 3):
season = Season.objects.create(anime=self.anime, number=s, title=f"Season {s}")
# Create 5 episodes per season
for e in range(1, 6):
episode = Episode.objects.create(season=season, number=e, title=f"S{s}E{e}")
# Create 3 video files per episode
for q in ['480p', '720p', '1080p']:
VideoFile.objects.create(
episode=episode,
quality=q,
hls_path=f"path/to/{q}.m3u8",
encryption_key="key"
)
# Create 2 external sources per episode
for src in ['hianime', 'zoro']:
ExternalSource.objects.create(
episode=episode,
source_type=src,
embed_url=f"https://example.com/{src}"
)
# The URL for anime detail viewset
# Using the router in aniscrap_core/urls.py
# router.register(r'anime', AnimeViewSet) -> 'anime-detail'
# But verify the reverse name. Usually it's basename-detail.
# If no basename provided, it defaults to queryset model name lowercased.
# Here Anime -> anime -> anime-detail.
self.url = f'/api/v1/anime/{self.anime.id}/'
def test_anime_detail_query_count(self):
# Warm up
self.client.get(self.url)
with CaptureQueriesContext(connection) as ctx:
response = self.client.get(self.url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Analyze queries
video_file_queries = [q for q in ctx.captured_queries if 'content_videofile' in q['sql']]
external_source_queries = [q for q in ctx.captured_queries if 'content_externalsource' in q['sql']]
# We expect 1 query for VideoFiles and 1 query for ExternalSources due to prefetch
self.assertEqual(len(video_file_queries), 1, "Should have exactly 1 query for VideoFiles")
self.assertEqual(len(external_source_queries), 1, "Should have exactly 1 query for ExternalSources")
# Total queries should be low (around 7-9)
# 1 Anime + 1 Genres + 1 Seasons + 1 Episodes + 1 AnimeCharacters + 1 Characters + 1 VideoFiles + 1 ExternalSources + maybe 1 session/auth
self.assertLessEqual(len(ctx.captured_queries), 12, f"Total queries too high: {len(ctx.captured_queries)}")
# Verify response structure
data = response.data
self.assertIn('seasons', data)
self.assertEqual(len(data['seasons']), 2)
season_data = data['seasons'][0]
self.assertIn('name', season_data) # Check if 'name' is present (mapped from 'title')
self.assertIn('episodes', season_data)
episode_data = season_data['episodes'][0]
self.assertIn('video_files', episode_data)
self.assertIn('external_sources', episode_data)
video_files = episode_data['video_files']
self.assertEqual(len(video_files), 3)
self.assertIn('file_url', video_files[0]) # Check if 'file_url' is present (mapped from 'hls_path')
external_sources = episode_data['external_sources']
self.assertEqual(len(external_sources), 2)
self.assertIn('source_name', external_sources[0]) # Check if 'source_name' is present (mapped from 'source_type')