Skip to content

Commit 901cd72

Browse files
committed
optimise view performance with prefetching imageversions and thumbnails, change ImageVersion fk from Image to BaseFile to make prefetching possible across polymorphic models
1 parent 45944b6 commit 901cd72

File tree

4 files changed

+64
-31
lines changed

4 files changed

+64
-31
lines changed

src/files/managers.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class BaseFileManager(PolymorphicManager):
1919
"""Custom manager for file operations."""
2020

2121
def get_queryset(self) -> models.QuerySet["BaseFile"]:
22-
"""Prefetch active albums into a list."""
22+
"""Prefetch and annotate."""
2323
return ( # type: ignore[no-any-return]
2424
super()
2525
.get_queryset()
@@ -36,6 +36,8 @@ def get_queryset(self) -> models.QuerySet["BaseFile"]:
3636
.annotate(jobs_unfinished=Count("jobs", filter=models.Q(jobs__finished=False)))
3737
.prefetch_active_albums_list(recursive=True)
3838
.prefetch_related("thumbnails")
39+
.prefetch_related(models.Prefetch("thumbnails", to_attr="thumbnail_list"))
40+
.prefetch_related(models.Prefetch("image_versions", to_attr="image_version_list"))
3941
)
4042

4143

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 5.1.4 on 2024-12-08 07:37
2+
3+
import utils.models
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('files', '0004_initial'),
11+
('images', '0002_initial'),
12+
]
13+
14+
operations = [
15+
migrations.AlterField(
16+
model_name='imageversion',
17+
name='image',
18+
field=models.ForeignKey(help_text='The Image this is a smaller version of.', on_delete=utils.models.NP_CASCADE, related_name='image_versions', to='files.basefile'),
19+
),
20+
]

src/images/models.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,15 @@ class Image(BaseFile):
6767
)
6868

6969
def get_fullsize_version(self, mimetype: str) -> "ImageVersion | None":
70-
"""Return the ImageVersion for the fullsize version of this mimetype for this Image."""
71-
try:
72-
return self.image_versions.get(width=self.width, aspect_ratio=self.aspect_ratio, mimetype=mimetype) # type: ignore[no-any-return]
73-
except ImageVersion.DoesNotExist:
74-
return None
70+
"""Return the ImageVersion for the fullsize version of this mimetype for this Image.
71+
72+
Performance sensitive, called from template tags, do not break prefetching. Loop over
73+
self.image_version_list instead of self.image_versions.filter().
74+
"""
75+
for image in self.image_version_list:
76+
if image.width == self.width and image.aspect_ratio == self.aspect_ratio and image.mimetype == mimetype:
77+
return image # type: ignore[no-any-return]
78+
return None
7579

7680
def create_jobs(self) -> None:
7781
"""Create jobs for exif, smaller versions and thumbnails for this image."""
@@ -131,7 +135,11 @@ def create_smaller_version_jobs(self) -> None:
131135
def get_versions(
132136
self, mimetype: str | None = None, aspect_ratio: Fraction | None = None
133137
) -> dict[Fraction | None, dict[str, dict[int, "ImageVersion"]]]:
134-
"""Get image versions. Return a dict with ratio: mimetype: size: ImageVersion dicts."""
138+
"""Get image versions. Return a dict with ratio: mimetype: size: ImageVersion dicts.
139+
140+
Performance sensitive, called from template tags, do not break prefetching. Loop over
141+
self.image_version_list instead of self.image_versions.filter().
142+
"""
135143
versions = {}
136144
kwargs = {
137145
"aspect_ratio": aspect_ratio or self.aspect_ratio,
@@ -140,7 +148,11 @@ def get_versions(
140148
if mimetype:
141149
kwargs["mimetype"] = mimetype
142150
# use requested custom AR or Image original AR
143-
for version in self.image_versions.filter(**kwargs):
151+
for version in self.image_version_list:
152+
if version.aspect_ratio != kwargs["aspect_ratio"]:
153+
continue
154+
if "mimetype" in kwargs and version.mimetype != kwargs["mimetype"]:
155+
continue
144156
if version.aspect_ratio not in versions:
145157
versions[version.aspect_ratio] = {}
146158
if version.mimetype not in versions[version.aspect_ratio]:
@@ -229,7 +241,7 @@ class ImageVersion(ImageModel, BaseModel):
229241
)
230242

231243
image = models.ForeignKey(
232-
"images.Image",
244+
"files.BaseFile",
233245
on_delete=NP_CASCADE, # delete all versions when an Image is deleted
234246
related_name="image_versions",
235247
help_text="The Image this is a smaller version of.",

src/utils/templatetags/bma_utils.py

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from django.template.context import RequestContext
1010
from django.utils.safestring import mark_safe
1111

12-
from files.models import Thumbnail
1312
from pictures.templatetags.pictures import picture
1413
from pictures.utils import sizes
1514

@@ -39,7 +38,7 @@ def get_group_icons(
3938

4039

4140
@register.simple_tag()
42-
def thumbnail(basefile: "BaseFile", width: int, ratio: str) -> str:
41+
def thumbnail(basefile: "BaseFile", width: int, ratio: str, mimetype: str = "image/webp") -> str:
4342
"""BMA thumbnail tag. Depends on the hardcoded 50,100,150,200px (and 2x)."""
4443
from files.models import ThumbnailSource
4544

@@ -54,31 +53,31 @@ def thumbnail(basefile: "BaseFile", width: int, ratio: str) -> str:
5453
f"<!-- Error creating thumbnail markup, aspect ratio {ratio} is not supported, "
5554
f"only {ThumbnailSource.source.field.aspect_ratios} are supported -->" # type: ignore[attr-defined]
5655
)
57-
58-
thumbnails = basefile.thumbnails.filter(
59-
width__in=[width, width * 2], aspect_ratio=str(Fraction(ratio)), mimetype="image/webp"
60-
)
61-
if not thumbnails:
62-
# neither requested size or 2x available, return default
63-
return mark_safe( # noqa: S308
64-
'<img class="img-fluid img-thumbnail" '
65-
f'src="{settings.DEFAULT_THUMBNAIL_URLS[basefile.filetype]}" width="{width}">'
66-
)
67-
try:
68-
t = thumbnails.get(width=width, height=width / Fraction(ratio))
69-
url = t.imagefile.url
70-
except Thumbnail.DoesNotExist:
71-
# requested size not available, return default
56+
t = None
57+
t2 = None
58+
for thumbnail in basefile.thumbnail_list:
59+
if thumbnail.mimetype != mimetype:
60+
continue
61+
if thumbnail.aspect_ratio != str(Fraction(ratio)):
62+
continue
63+
if thumbnail.width == width:
64+
t = thumbnail
65+
continue
66+
if thumbnail.width == width * 2:
67+
t2 = thumbnail
68+
continue
69+
70+
if not t:
71+
# request size not available
7272
return mark_safe( # noqa: S308
7373
'<img class="img-fluid img-thumbnail" '
7474
f'src="{settings.DEFAULT_THUMBNAIL_URLS[basefile.filetype]}" width="{width}">'
7575
)
76-
77-
try:
78-
url2x = thumbnails.get(width=width * 2).imagefile.url
76+
url = t.imagefile.url
77+
if t2:
78+
url2x = t2.imagefile.url
7979
url2x = f", {url2x} 2x"
80-
except Thumbnail.DoesNotExist:
81-
# 2x requested size not available, skip 2x for this thumbnail
80+
else:
8281
url2x = ""
8382

8483
title = basefile.original_filename

0 commit comments

Comments
 (0)