Fix #865: Performance optimization on /artists, /albums and /tracks endpoints

This commit is contained in:
Eliot Berriot 2019-09-24 09:48:04 +02:00
parent 8ed6f830b5
commit 81349e2b57
5 changed files with 84 additions and 77 deletions

View File

@ -104,58 +104,50 @@ class ArtistWithAlbumsSerializer(serializers.ModelSerializer):
get_attributed_to = serialize_attributed_to get_attributed_to = serialize_attributed_to
def get_tracks_count(self, o): def get_tracks_count(self, o):
return getattr(o, "_tracks_count", None) tracks = getattr(o, "_prefetched_tracks", None)
return len(tracks) if tracks else None
class ArtistSimpleSerializer(serializers.ModelSerializer): def serialize_artist_simple(artist):
class Meta: return {
model = models.Artist "id": artist.id,
fields = ("id", "fid", "mbid", "name", "creation_date", "is_local") "fid": artist.fid,
"mbid": str(artist.mbid),
"name": artist.name,
"creation_date": serializers.DateTimeField().to_representation(
artist.creation_date
),
"is_local": artist.is_local,
}
class AlbumTrackSerializer(serializers.ModelSerializer): def serialize_album_track(track):
artist = ArtistSimpleSerializer(read_only=True) return {
uploads = serializers.SerializerMethodField() "id": track.id,
listen_url = serializers.SerializerMethodField() "fid": track.fid,
duration = serializers.SerializerMethodField() "mbid": str(track.mbid),
"title": track.title,
class Meta: "artist": serialize_artist_simple(track.artist),
model = models.Track "album": track.album_id,
fields = ( "creation_date": serializers.DateTimeField().to_representation(
"id", track.creation_date
"fid", ),
"mbid", "position": track.position,
"title", "disc_number": track.disc_number,
"album", "uploads": [
"artist", serialize_upload(u) for u in getattr(track, "playable_uploads", [])
"creation_date", ],
"position", "listen_url": track.listen_url,
"disc_number", "duration": getattr(track, "duration", None),
"uploads", "copyright": track.copyright,
"listen_url", "license": track.license_id,
"duration", "is_local": track.is_local,
"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
class AlbumSerializer(serializers.ModelSerializer): class AlbumSerializer(serializers.ModelSerializer):
tracks = serializers.SerializerMethodField() tracks = serializers.SerializerMethodField()
artist = ArtistSimpleSerializer(read_only=True) artist = serializers.SerializerMethodField()
cover = cover_field cover = cover_field
is_playable = serializers.SerializerMethodField() is_playable = serializers.SerializerMethodField()
tags = serializers.SerializerMethodField() tags = serializers.SerializerMethodField()
@ -181,9 +173,12 @@ class AlbumSerializer(serializers.ModelSerializer):
get_attributed_to = serialize_attributed_to get_attributed_to = serialize_attributed_to
def get_artist(self, o):
return serialize_artist_simple(o.artist)
def get_tracks(self, o): def get_tracks(self, o):
ordered_tracks = o.tracks.all() 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): def get_is_playable(self, obj):
try: try:
@ -199,7 +194,7 @@ class AlbumSerializer(serializers.ModelSerializer):
class TrackAlbumSerializer(serializers.ModelSerializer): class TrackAlbumSerializer(serializers.ModelSerializer):
artist = ArtistSimpleSerializer(read_only=True) artist = serializers.SerializerMethodField()
cover = cover_field cover = cover_field
class Meta: class Meta:
@ -216,23 +211,24 @@ class TrackAlbumSerializer(serializers.ModelSerializer):
"is_local", "is_local",
) )
def get_artist(self, o):
return serialize_artist_simple(o.artist)
class TrackUploadSerializer(serializers.ModelSerializer):
class Meta: def serialize_upload(upload):
model = models.Upload return {
fields = ( "uuid": str(upload.uuid),
"uuid", "listen_url": upload.listen_url,
"listen_url", "size": upload.size,
"size", "duration": upload.duration,
"duration", "bitrate": upload.bitrate,
"bitrate", "mimetype": upload.mimetype,
"mimetype", "extension": upload.extension,
"extension", }
)
class TrackSerializer(serializers.ModelSerializer): class TrackSerializer(serializers.ModelSerializer):
artist = ArtistSimpleSerializer(read_only=True) artist = serializers.SerializerMethodField()
album = TrackAlbumSerializer(read_only=True) album = TrackAlbumSerializer(read_only=True)
uploads = serializers.SerializerMethodField() uploads = serializers.SerializerMethodField()
listen_url = serializers.SerializerMethodField() listen_url = serializers.SerializerMethodField()
@ -262,12 +258,14 @@ class TrackSerializer(serializers.ModelSerializer):
get_attributed_to = serialize_attributed_to get_attributed_to = serialize_attributed_to
def get_artist(self, o):
return serialize_artist_simple(o.artist)
def get_listen_url(self, obj): def get_listen_url(self, obj):
return obj.listen_url return obj.listen_url
def get_uploads(self, obj): def get_uploads(self, obj):
uploads = getattr(obj, "playable_uploads", []) return [serialize_upload(u) for u in getattr(obj, "playable_uploads", [])]
return TrackUploadSerializer(uploads, many=True).data
def get_tags(self, obj): def get_tags(self, obj):
tagged_items = getattr(obj, "_prefetched_tagged_items", []) tagged_items = getattr(obj, "_prefetched_tagged_items", [])

View File

@ -96,8 +96,14 @@ def refetch_obj(obj, queryset):
class ArtistViewSet(common_views.SkipFilterForGetObject, viewsets.ReadOnlyModelViewSet): class ArtistViewSet(common_views.SkipFilterForGetObject, viewsets.ReadOnlyModelViewSet):
queryset = ( queryset = (
models.Artist.objects.all() models.Artist.objects.all()
.select_related("attributed_to") .prefetch_related("attributed_to")
.annotate(_tracks_count=Count("tracks")) .prefetch_related(
Prefetch(
"tracks",
queryset=models.Track.objects.all(),
to_attr="_prefetched_tracks",
)
)
) )
serializer_class = serializers.ArtistWithAlbumsSerializer serializer_class = serializers.ArtistWithAlbumsSerializer
permission_classes = [oauth_permissions.ScopePermission] permission_classes = [oauth_permissions.ScopePermission]

View File

@ -62,7 +62,7 @@ def test_artist_with_albums_serializer(factories, to_api_date):
artist = track.artist artist = track.artist
artist = artist.__class__.objects.with_albums().get(pk=artist.pk) artist = artist.__class__.objects.with_albums().get(pk=artist.pk)
album = list(artist.albums.all())[0] album = list(artist.albums.all())[0]
setattr(artist, "_tracks_count", 42) setattr(artist, "_prefetched_tracks", range(42))
expected = { expected = {
"id": artist.id, "id": artist.id,
"fid": artist.fid, "fid": artist.fid,
@ -89,13 +89,13 @@ def test_album_track_serializer(factories, to_api_date):
expected = { expected = {
"id": track.id, "id": track.id,
"fid": track.fid, "fid": track.fid,
"artist": serializers.ArtistSimpleSerializer(track.artist).data, "artist": serializers.serialize_artist_simple(track.artist),
"album": track.album.id, "album": track.album.id,
"mbid": str(track.mbid), "mbid": str(track.mbid),
"title": track.title, "title": track.title,
"position": track.position, "position": track.position,
"disc_number": track.disc_number, "disc_number": track.disc_number,
"uploads": [serializers.TrackUploadSerializer(upload).data], "uploads": [serializers.serialize_upload(upload)],
"creation_date": to_api_date(track.creation_date), "creation_date": to_api_date(track.creation_date),
"listen_url": track.listen_url, "listen_url": track.listen_url,
"duration": None, "duration": None,
@ -103,8 +103,8 @@ def test_album_track_serializer(factories, to_api_date):
"copyright": track.copyright, "copyright": track.copyright,
"is_local": track.is_local, "is_local": track.is_local,
} }
serializer = serializers.AlbumTrackSerializer(track) data = serializers.serialize_album_track(track)
assert serializer.data == expected assert data == expected
def test_upload_serializer(factories, to_api_date): def test_upload_serializer(factories, to_api_date):
@ -169,7 +169,7 @@ def test_album_serializer(factories, to_api_date):
"fid": album.fid, "fid": album.fid,
"mbid": str(album.mbid), "mbid": str(album.mbid),
"title": album.title, "title": album.title,
"artist": serializers.ArtistSimpleSerializer(album.artist).data, "artist": serializers.serialize_artist_simple(album.artist),
"creation_date": to_api_date(album.creation_date), "creation_date": to_api_date(album.creation_date),
"is_playable": False, "is_playable": False,
"cover": { "cover": {
@ -179,7 +179,7 @@ def test_album_serializer(factories, to_api_date):
"small_square_crop": album.cover.crop["50x50"].url, "small_square_crop": album.cover.crop["50x50"].url,
}, },
"release_date": to_api_date(album.release_date), "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, "is_local": album.is_local,
"tags": [], "tags": [],
"attributed_to": federation_serializers.APIActorSerializer(actor).data, "attributed_to": federation_serializers.APIActorSerializer(actor).data,
@ -202,13 +202,13 @@ def test_track_serializer(factories, to_api_date):
expected = { expected = {
"id": track.id, "id": track.id,
"fid": track.fid, "fid": track.fid,
"artist": serializers.ArtistSimpleSerializer(track.artist).data, "artist": serializers.serialize_artist_simple(track.artist),
"album": serializers.TrackAlbumSerializer(track.album).data, "album": serializers.TrackAlbumSerializer(track.album).data,
"mbid": str(track.mbid), "mbid": str(track.mbid),
"title": track.title, "title": track.title,
"position": track.position, "position": track.position,
"disc_number": track.disc_number, "disc_number": track.disc_number,
"uploads": [serializers.TrackUploadSerializer(upload).data], "uploads": [serializers.serialize_upload(upload)],
"creation_date": to_api_date(track.creation_date), "creation_date": to_api_date(track.creation_date),
"listen_url": track.listen_url, "listen_url": track.listen_url,
"license": upload.track.license.code, "license": upload.track.license.code,
@ -317,7 +317,7 @@ def test_manage_upload_action_relaunch_import(factories, mocker):
assert m.call_count == 3 assert m.call_count == 3
def test_track_upload_serializer(factories): def test_serialize_upload(factories):
upload = factories["music.Upload"]() upload = factories["music.Upload"]()
expected = { expected = {
@ -330,8 +330,8 @@ def test_track_upload_serializer(factories):
"duration": upload.duration, "duration": upload.duration,
} }
serializer = serializers.TrackUploadSerializer(upload) data = serializers.serialize_upload(upload)
assert serializer.data == expected assert data == expected
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -6,7 +6,7 @@ import urllib.parse
import uuid import uuid
import pytest import pytest
from django.db.models import Count from django.db.models import Prefetch
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
@ -28,7 +28,9 @@ def test_artist_list_serializer(api_request, factories, logged_in_api_client):
).track ).track
artist = track.artist artist = track.artist
request = api_request.get("/") 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( serializer = serializers.ArtistWithAlbumsSerializer(
qs, many=True, context={"request": request} qs, many=True, context={"request": request}
) )

View File

@ -0,0 +1 @@
Improved performance of /artists, /albums and /tracks API endpoints by a factor 2 (#865)