diff --git a/vrobbler/apps/people/tests/test_person_scrobble_performance.py b/vrobbler/apps/people/tests/test_person_scrobble_performance.py new file mode 100644 index 0000000..e4bea57 --- /dev/null +++ b/vrobbler/apps/people/tests/test_person_scrobble_performance.py @@ -0,0 +1,196 @@ +import pytest +from django.contrib.auth import get_user_model +from django.test.utils import CaptureQueriesContext +from django.db import connection +from django.utils import timezone + +from music.aggregators import batch_live_charts, live_charts +from scrobbles.models import Scrobble, Track, Artist +from videos.models import Video + +User = get_user_model() + + +@pytest.mark.django_db +def test_scrobble_queryset_with_related_reduces_queries(): + """Test that Scrobble.objects.with_related() reduces query count.""" + user = User.objects.create_user(username="perfuser", password="testpass123") + + artist = Artist.objects.create(name="Test Artist") + track = Track.objects.create( + title="Test Track", artist=artist, base_run_time_seconds=180 + ) + + num_scrobbles = 10 + for i in range(num_scrobbles): + Scrobble.objects.create( + user=user, + track=track, + media_type=Scrobble.MediaType.TRACK, + played_to_completion=True, + timestamp=timezone.now() - timezone.timedelta(days=i), + ) + + with CaptureQueriesContext(connection) as context: + scrobbles = list(Scrobble.objects.with_related().filter(user=user)) + for s in scrobbles: + _ = s.track.title + _ = s.track.artist.name + + print( + f"\nwith_related() queries for {num_scrobbles} scrobbles: {len(context.captured_queries)}" + ) + for q in context.captured_queries: + print(f" {q['sql'][:100]}...") + + assert len(context.captured_queries) < num_scrobbles + + +@pytest.mark.django_db +def test_batch_live_charts_vs_individual_calls(): + """Test that batch_live_charts uses fewer queries than individual live_charts calls.""" + user = User.objects.create_user(username="batchuser", password="testpass123") + + artist = Artist.objects.create(name="Test Artist") + track = Track.objects.create( + title="Test Track", artist=artist, base_run_time_seconds=180 + ) + + for i in range(5): + Scrobble.objects.create( + user=user, + track=track, + media_type=Scrobble.MediaType.TRACK, + played_to_completion=True, + timestamp=timezone.now() - timezone.timedelta(days=i), + ) + + with CaptureQueriesContext(connection) as batch_context: + batch_result = batch_live_charts(user=user, media_type="Track", limit=15) + + with CaptureQueriesContext(connection) as individual_context: + individual_result = { + "today": list( + live_charts(user=user, media_type="Track", chart_period="today") + ), + "last7": list( + live_charts(user=user, media_type="Track", chart_period="last7") + ), + "last30": list( + live_charts(user=user, media_type="Track", chart_period="last30") + ), + "year": list( + live_charts(user=user, media_type="Track", chart_period="year") + ), + "all": list(live_charts(user=user, media_type="Track", chart_period="all")), + } + + print(f"\nbatch_live_charts queries: {len(batch_context.captured_queries)}") + print(f"individual live_charts queries: {len(individual_context.captured_queries)}") + + assert len(batch_context.captured_queries) <= len( + individual_context.captured_queries + ) + + +@pytest.mark.django_db +def test_as_dict_by_type_uses_with_related(): + """Test that as_dict_by_type uses with_related() for reduced queries.""" + user = User.objects.create_user(username="dictuser", password="testpass123") + + artist = Artist.objects.create(name="Test Artist") + track = Track.objects.create( + title="Test Track", artist=artist, base_run_time_seconds=180 + ) + + for i in range(5): + Scrobble.objects.create( + user=user, + track=track, + media_type=Scrobble.MediaType.TRACK, + played_to_completion=True, + timestamp=timezone.now() - timezone.timedelta(days=i), + ) + + scrobbles = Scrobble.objects.filter(user=user) + + with CaptureQueriesContext(connection) as context: + result = Scrobble.as_dict_by_type(scrobbles) + + print(f"\nas_dict_by_type queries: {len(context.captured_queries)}") + for q in context.captured_queries: + print(f" {q['sql'][:100]}...") + + assert len(context.captured_queries) < 10 + assert "Track" in result + assert len(result["Track"]) == 5 + + +@pytest.mark.django_db +def test_person_scrobble_junction_efficient_count(): + """Test that PersonScrobble junction table enables efficient counts.""" + from people.models import Person, PersonScrobble + + creator = User.objects.create_user(username="creator2", password="testpass123") + other_user = User.objects.create_user(username="other2", password="testpass123") + + person = Person.objects.create(name="Test Person", created_by=creator, user=creator) + + artist = Artist.objects.create(name="Test Artist") + track = Track.objects.create( + title="Test Track", artist=artist, base_run_time_seconds=180 + ) + + for i in range(10): + scrobble = Scrobble.objects.create( + user=other_user, + track=track, + media_type=Scrobble.MediaType.TRACK, + played_to_completion=True, + log={"with_people_ids": [person.id]}, + ) + + count = person.get_scrobble_count(user=other_user) + assert count == 10 + + with CaptureQueriesContext(connection) as context: + assert person.get_scrobble_count(user=other_user) == 10 + + print(f"\nPersonScrobble count queries: {len(context.captured_queries)}") + for q in context.captured_queries: + print(f" {q['sql'][:100]}...") + + assert len(context.captured_queries) <= 3 + + +@pytest.mark.django_db +def test_video_scrobbles_with_related(): + """Test prefetching works for video scrobbles.""" + user = User.objects.create_user(username="videouser", password="testpass123") + + video = Video.objects.create( + title="Test Video", + video_type=Video.VideoType.MOVIE, + base_run_time_seconds=5400, + ) + + for i in range(3): + Scrobble.objects.create( + user=user, + video=video, + media_type=Scrobble.MediaType.VIDEO, + played_to_completion=True, + timestamp=timezone.now() - timezone.timedelta(days=i), + ) + + with CaptureQueriesContext(connection) as context: + scrobbles = list(Scrobble.objects.with_related().filter(user=user)) + for s in scrobbles: + _ = s.video.title + _ = s.video.video_type + + print(f"\nVideo scrobbles with_related() queries: {len(context.captured_queries)}") + for q in context.captured_queries: + print(f" {q['sql'][:100]}...") + + assert len(context.captured_queries) < 5