From f37da8f7d88701dffc6d4beb0e63270ec9470ad1 Mon Sep 17 00:00:00 2001 From: Tom Dyson Date: Thu, 1 Nov 2018 23:26:40 +0000 Subject: [PATCH] Cache image renditions (#4883) --- CHANGELOG.txt | 1 + docs/advanced_topics/performance.rst | 22 ++++++++++++++++++++ docs/releases/2.9.rst | 1 + wagtail/images/models.py | 20 +++++++++++++++++++ wagtail/images/tests/test_models.py | 30 +++++++++++++++++++++++++++- 5 files changed, 73 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 349951d9ad..0b83566652 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -25,6 +25,7 @@ Changelog * ListBlocks now call child block `bulk_to_python` if defined (Andy Chosak) * Site settings are now identifiable/cachable by request as well as site (Andy Babic) * Added `select_related` attribute to site settings to enable more efficient fetching of foreign key values (Andy Babic) + * Add caching of image renditions (Tom Dyson, Tim Kamanin) * Fix: Added ARIA alert role to live search forms in the admin (Casper Timmers) * Fix: Reorder login form elements to match expected tab order (Kjartan Sverrisson) * Fix: Re-add 'Close Explorer' button on mobile viewports (Sævar Öfjörð Magnússon) diff --git a/docs/advanced_topics/performance.rst b/docs/advanced_topics/performance.rst index 6383e49faa..0875828e3e 100644 --- a/docs/advanced_topics/performance.rst +++ b/docs/advanced_topics/performance.rst @@ -30,6 +30,28 @@ We recommend `Redis `_ as a fast, persistent cache. Install R } +Caching image renditions +------------------------ + +If you define a cache named 'renditions' (typically alongside your 'default' cache), +Wagtail will cache image rendition lookups, which may improve the performance of pages +which include many images. + +.. code-block:: python + + CACHES = { + 'default': {...}, + 'renditions': { + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'LOCATION': '127.0.0.1:11211', + 'TIMEOUT': 600, + 'OPTIONS': { + 'MAX_ENTRIES': 1000 + } + } + } + + Search ------ diff --git a/docs/releases/2.9.rst b/docs/releases/2.9.rst index 5d936498c9..1315cc0715 100644 --- a/docs/releases/2.9.rst +++ b/docs/releases/2.9.rst @@ -38,6 +38,7 @@ Other features * ListBLocks now call child block ``bulk_to_python`` if defined (Andy Chosak) * Site settings are now identifiable/cachable by request as well as site (Andy Babic) * Added ``select_related`` attribute to site settings to enable more efficient fetching of foreign key values (Andy Babic) + * Add caching of image renditions (Tom Dyson, Tim Kamanin) Bug fixes diff --git a/wagtail/images/models.py b/wagtail/images/models.py index 402b96c3d9..5e0f2773f3 100644 --- a/wagtail/images/models.py +++ b/wagtail/images/models.py @@ -6,6 +6,7 @@ from io import BytesIO from django.conf import settings from django.core import checks +from django.core.cache import InvalidCacheBackendError, caches from django.core.files import File from django.db import models from django.forms.utils import flatatt @@ -277,6 +278,22 @@ class AbstractImage(CollectionMember, index.Indexed, models.Model): filter = Filter(spec=filter) cache_key = filter.get_cache_key(self) + + + try: + rendition_caching = True + cache = caches['renditions'] + rendition_cache_key = "image-{}-{}-{}".format( + self.id, + cache_key, + filter.spec + ) + cached_rendition = cache.get(rendition_cache_key) + if cached_rendition: + return cached_rendition + except InvalidCacheBackendError: + rendition_caching = False + Rendition = self.get_rendition_model() try: @@ -314,6 +331,9 @@ class AbstractImage(CollectionMember, index.Indexed, models.Model): defaults={'file': File(generated_image.f, name=output_filename)} ) + if rendition_caching: + cache.set(rendition_cache_key, rendition) + return rendition def is_portrait(self): diff --git a/wagtail/images/tests/test_models.py b/wagtail/images/tests/test_models.py index ef54fe32df..1f55f68354 100644 --- a/wagtail/images/tests/test_models.py +++ b/wagtail/images/tests/test_models.py @@ -2,6 +2,7 @@ import unittest from django.contrib.auth import get_user_model from django.contrib.auth.models import Group, Permission +from django.core.cache import caches from django.core.files.uploadedfile import SimpleUploadedFile from django.db.utils import IntegrityError from django.test import TestCase @@ -14,7 +15,6 @@ from wagtail.images.models import Rendition, SourceImageIOError from wagtail.images.rect import Rect from wagtail.tests.testapp.models import EventPage, EventPageCarouselItem from wagtail.tests.utils import WagtailTestUtils - from .utils import Image, get_test_image_file @@ -252,6 +252,32 @@ class TestRenditions(TestCase): rendition = self.image.get_rendition('width-400') self.assertEqual(rendition.alt, "Test image") + @override_settings( + CACHES={ + 'renditions': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + }, + }, + ) + def test_renditions_cache_backend(self): + cache = caches['renditions'] + rendition = self.image.get_rendition('width-500') + rendition_cache_key = "image-{}-{}-{}".format( + rendition.image.id, + rendition.focal_point_key, + rendition.filter_spec + ) + + # Check rendition is saved to cache + self.assertEqual(cache.get(rendition_cache_key), rendition) + + # Mark a rendition to check it comes from cache + rendition._from_cache = True + cache.set(rendition_cache_key, rendition) + + # Check if get_rendition returns the rendition from cache + self.assertEqual(self.image.get_rendition('width-500')._from_cache, True) + class TestUsageCount(TestCase): fixtures = ['test.json'] @@ -360,6 +386,7 @@ class TestIssue573(TestCase): This tests for a bug which causes filename limit on Renditions to be reached when the Image has a long original filename and a big focal point key """ + def test_issue_573(self): # Create an image with a big filename and focal point image = Image.objects.create( @@ -500,6 +527,7 @@ class TestFilenameReduction(TestCase): This tests for a bug which results in filenames without extensions causing an infinite loop """ + def test_filename_reduction_no_ext(self): # Create an image with a big filename and no extension image = Image.objects.create(