From 81349e2b57cd0e8c4a6d9bb4d8a2ed2fe3c7f100 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Tue, 24 Sep 2019 09:48:04 +0200
Subject: [PATCH] Fix #865: Performance optimization on /artists, /albums and
 /tracks endpoints

---
 api/funkwhale_api/music/serializers.py | 128 ++++++++++++-------------
 api/funkwhale_api/music/views.py       |  10 +-
 api/tests/music/test_serializers.py    |  24 ++---
 api/tests/music/test_views.py          |   6 +-
 changes/changelog.d/865.bugfix         |   1 +
 5 files changed, 88 insertions(+), 81 deletions(-)
 create mode 100644 changes/changelog.d/865.bugfix

diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py
index 630f9f4e1..77854d437 100644
--- a/api/funkwhale_api/music/serializers.py
+++ b/api/funkwhale_api/music/serializers.py
@@ -104,58 +104,50 @@ class ArtistWithAlbumsSerializer(serializers.ModelSerializer):
     get_attributed_to = serialize_attributed_to
 
     def get_tracks_count(self, o):
-        return getattr(o, "_tracks_count", None)
-
-
-class ArtistSimpleSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = models.Artist
-        fields = ("id", "fid", "mbid", "name", "creation_date", "is_local")
-
-
-class AlbumTrackSerializer(serializers.ModelSerializer):
-    artist = ArtistSimpleSerializer(read_only=True)
-    uploads = serializers.SerializerMethodField()
-    listen_url = serializers.SerializerMethodField()
-    duration = serializers.SerializerMethodField()
-
-    class Meta:
-        model = models.Track
-        fields = (
-            "id",
-            "fid",
-            "mbid",
-            "title",
-            "album",
-            "artist",
-            "creation_date",
-            "position",
-            "disc_number",
-            "uploads",
-            "listen_url",
-            "duration",
-            "copyright",
-            "license",
-            "is_local",
-        )
-
-    def get_uploads(self, obj):
-        uploads = getattr(obj, "playable_uploads", [])
-        return TrackUploadSerializer(uploads, many=True).data
-
-    def get_listen_url(self, obj):
-        return obj.listen_url
-
-    def get_duration(self, obj):
-        try:
-            return obj.duration
-        except AttributeError:
-            return None
+        tracks = getattr(o, "_prefetched_tracks", None)
+        return len(tracks) if tracks else None
+
+
+def serialize_artist_simple(artist):
+    return {
+        "id": artist.id,
+        "fid": artist.fid,
+        "mbid": str(artist.mbid),
+        "name": artist.name,
+        "creation_date": serializers.DateTimeField().to_representation(
+            artist.creation_date
+        ),
+        "is_local": artist.is_local,
+    }
+
+
+def serialize_album_track(track):
+    return {
+        "id": track.id,
+        "fid": track.fid,
+        "mbid": str(track.mbid),
+        "title": track.title,
+        "artist": serialize_artist_simple(track.artist),
+        "album": track.album_id,
+        "creation_date": serializers.DateTimeField().to_representation(
+            track.creation_date
+        ),
+        "position": track.position,
+        "disc_number": track.disc_number,
+        "uploads": [
+            serialize_upload(u) for u in getattr(track, "playable_uploads", [])
+        ],
+        "listen_url": track.listen_url,
+        "duration": getattr(track, "duration", None),
+        "copyright": track.copyright,
+        "license": track.license_id,
+        "is_local": track.is_local,
+    }
 
 
 class AlbumSerializer(serializers.ModelSerializer):
     tracks = serializers.SerializerMethodField()
-    artist = ArtistSimpleSerializer(read_only=True)
+    artist = serializers.SerializerMethodField()
     cover = cover_field
     is_playable = serializers.SerializerMethodField()
     tags = serializers.SerializerMethodField()
@@ -181,9 +173,12 @@ class AlbumSerializer(serializers.ModelSerializer):
 
     get_attributed_to = serialize_attributed_to
 
+    def get_artist(self, o):
+        return serialize_artist_simple(o.artist)
+
     def get_tracks(self, o):
         ordered_tracks = o.tracks.all()
-        return AlbumTrackSerializer(ordered_tracks, many=True).data
+        return [serialize_album_track(track) for track in ordered_tracks]
 
     def get_is_playable(self, obj):
         try:
@@ -199,7 +194,7 @@ class AlbumSerializer(serializers.ModelSerializer):
 
 
 class TrackAlbumSerializer(serializers.ModelSerializer):
-    artist = ArtistSimpleSerializer(read_only=True)
+    artist = serializers.SerializerMethodField()
     cover = cover_field
 
     class Meta:
@@ -216,23 +211,24 @@ class TrackAlbumSerializer(serializers.ModelSerializer):
             "is_local",
         )
 
+    def get_artist(self, o):
+        return serialize_artist_simple(o.artist)
 
-class TrackUploadSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = models.Upload
-        fields = (
-            "uuid",
-            "listen_url",
-            "size",
-            "duration",
-            "bitrate",
-            "mimetype",
-            "extension",
-        )
+
+def serialize_upload(upload):
+    return {
+        "uuid": str(upload.uuid),
+        "listen_url": upload.listen_url,
+        "size": upload.size,
+        "duration": upload.duration,
+        "bitrate": upload.bitrate,
+        "mimetype": upload.mimetype,
+        "extension": upload.extension,
+    }
 
 
 class TrackSerializer(serializers.ModelSerializer):
-    artist = ArtistSimpleSerializer(read_only=True)
+    artist = serializers.SerializerMethodField()
     album = TrackAlbumSerializer(read_only=True)
     uploads = serializers.SerializerMethodField()
     listen_url = serializers.SerializerMethodField()
@@ -262,12 +258,14 @@ class TrackSerializer(serializers.ModelSerializer):
 
     get_attributed_to = serialize_attributed_to
 
+    def get_artist(self, o):
+        return serialize_artist_simple(o.artist)
+
     def get_listen_url(self, obj):
         return obj.listen_url
 
     def get_uploads(self, obj):
-        uploads = getattr(obj, "playable_uploads", [])
-        return TrackUploadSerializer(uploads, many=True).data
+        return [serialize_upload(u) for u in getattr(obj, "playable_uploads", [])]
 
     def get_tags(self, obj):
         tagged_items = getattr(obj, "_prefetched_tagged_items", [])
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index 33d3663c1..4783a8d45 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -96,8 +96,14 @@ def refetch_obj(obj, queryset):
 class ArtistViewSet(common_views.SkipFilterForGetObject, viewsets.ReadOnlyModelViewSet):
     queryset = (
         models.Artist.objects.all()
-        .select_related("attributed_to")
-        .annotate(_tracks_count=Count("tracks"))
+        .prefetch_related("attributed_to")
+        .prefetch_related(
+            Prefetch(
+                "tracks",
+                queryset=models.Track.objects.all(),
+                to_attr="_prefetched_tracks",
+            )
+        )
     )
     serializer_class = serializers.ArtistWithAlbumsSerializer
     permission_classes = [oauth_permissions.ScopePermission]
diff --git a/api/tests/music/test_serializers.py b/api/tests/music/test_serializers.py
index 333881037..4eaf54ad5 100644
--- a/api/tests/music/test_serializers.py
+++ b/api/tests/music/test_serializers.py
@@ -62,7 +62,7 @@ def test_artist_with_albums_serializer(factories, to_api_date):
     artist = track.artist
     artist = artist.__class__.objects.with_albums().get(pk=artist.pk)
     album = list(artist.albums.all())[0]
-    setattr(artist, "_tracks_count", 42)
+    setattr(artist, "_prefetched_tracks", range(42))
     expected = {
         "id": artist.id,
         "fid": artist.fid,
@@ -89,13 +89,13 @@ def test_album_track_serializer(factories, to_api_date):
     expected = {
         "id": track.id,
         "fid": track.fid,
-        "artist": serializers.ArtistSimpleSerializer(track.artist).data,
+        "artist": serializers.serialize_artist_simple(track.artist),
         "album": track.album.id,
         "mbid": str(track.mbid),
         "title": track.title,
         "position": track.position,
         "disc_number": track.disc_number,
-        "uploads": [serializers.TrackUploadSerializer(upload).data],
+        "uploads": [serializers.serialize_upload(upload)],
         "creation_date": to_api_date(track.creation_date),
         "listen_url": track.listen_url,
         "duration": None,
@@ -103,8 +103,8 @@ def test_album_track_serializer(factories, to_api_date):
         "copyright": track.copyright,
         "is_local": track.is_local,
     }
-    serializer = serializers.AlbumTrackSerializer(track)
-    assert serializer.data == expected
+    data = serializers.serialize_album_track(track)
+    assert data == expected
 
 
 def test_upload_serializer(factories, to_api_date):
@@ -169,7 +169,7 @@ def test_album_serializer(factories, to_api_date):
         "fid": album.fid,
         "mbid": str(album.mbid),
         "title": album.title,
-        "artist": serializers.ArtistSimpleSerializer(album.artist).data,
+        "artist": serializers.serialize_artist_simple(album.artist),
         "creation_date": to_api_date(album.creation_date),
         "is_playable": False,
         "cover": {
@@ -179,7 +179,7 @@ def test_album_serializer(factories, to_api_date):
             "small_square_crop": album.cover.crop["50x50"].url,
         },
         "release_date": to_api_date(album.release_date),
-        "tracks": serializers.AlbumTrackSerializer([track2, track1], many=True).data,
+        "tracks": [serializers.serialize_album_track(t) for t in [track2, track1]],
         "is_local": album.is_local,
         "tags": [],
         "attributed_to": federation_serializers.APIActorSerializer(actor).data,
@@ -202,13 +202,13 @@ def test_track_serializer(factories, to_api_date):
     expected = {
         "id": track.id,
         "fid": track.fid,
-        "artist": serializers.ArtistSimpleSerializer(track.artist).data,
+        "artist": serializers.serialize_artist_simple(track.artist),
         "album": serializers.TrackAlbumSerializer(track.album).data,
         "mbid": str(track.mbid),
         "title": track.title,
         "position": track.position,
         "disc_number": track.disc_number,
-        "uploads": [serializers.TrackUploadSerializer(upload).data],
+        "uploads": [serializers.serialize_upload(upload)],
         "creation_date": to_api_date(track.creation_date),
         "listen_url": track.listen_url,
         "license": upload.track.license.code,
@@ -317,7 +317,7 @@ def test_manage_upload_action_relaunch_import(factories, mocker):
     assert m.call_count == 3
 
 
-def test_track_upload_serializer(factories):
+def test_serialize_upload(factories):
     upload = factories["music.Upload"]()
 
     expected = {
@@ -330,8 +330,8 @@ def test_track_upload_serializer(factories):
         "duration": upload.duration,
     }
 
-    serializer = serializers.TrackUploadSerializer(upload)
-    assert serializer.data == expected
+    data = serializers.serialize_upload(upload)
+    assert data == expected
 
 
 @pytest.mark.parametrize(
diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py
index b9064cb05..4a423ddb1 100644
--- a/api/tests/music/test_views.py
+++ b/api/tests/music/test_views.py
@@ -6,7 +6,7 @@ import urllib.parse
 import uuid
 
 import pytest
-from django.db.models import Count
+from django.db.models import Prefetch
 from django.urls import reverse
 from django.utils import timezone
 
@@ -28,7 +28,9 @@ def test_artist_list_serializer(api_request, factories, logged_in_api_client):
     ).track
     artist = track.artist
     request = api_request.get("/")
-    qs = artist.__class__.objects.with_albums().annotate(_tracks_count=Count("tracks"))
+    qs = artist.__class__.objects.with_albums().prefetch_related(
+        Prefetch("tracks", to_attr="_prefetched_tracks")
+    )
     serializer = serializers.ArtistWithAlbumsSerializer(
         qs, many=True, context={"request": request}
     )
diff --git a/changes/changelog.d/865.bugfix b/changes/changelog.d/865.bugfix
new file mode 100644
index 000000000..183530e0d
--- /dev/null
+++ b/changes/changelog.d/865.bugfix
@@ -0,0 +1 @@
+Improved performance of /artists, /albums and /tracks API endpoints by a factor 2 (#865)
-- 
GitLab