Repository: wagtail/wagtail

Issue:
Report pages performance regression
### Issue Summary

Various report pages have a performance regression in Wagtail 5.2, which I've tracked down to:

https://github.com/wagtail/wagtail/commit/7ba1afb8a402a09be5838a026523be78f08ea877
https://github.com/wagtail/wagtail/pull/10822

On a few sites we've upgraded to Wagtail 5.2 - performance in the Site History report has been significantly reduced:

Before:
<img width="1717" alt="Screenshot 2023-11-11 at 21 12 02" src="https://github.com/wagtail/wagtail/assets/177332/79650e6b-9c96-4d21-bbdf-23b98c862bf4">

After:
<img width="1716" alt="Screenshot 2023-11-11 at 21 13 09" src="https://github.com/wagtail/wagtail/assets/177332/e719e250-5c9c-4dc8-823b-1e1c3b40a74c">
<img width="900" alt="Screenshot 2023-11-11 at 21 13 19" src="https://github.com/wagtail/wagtail/assets/177332/5623467b-a0ca-4472-aa46-540ff568ac82">

### Steps to Reproduce

Find an existing Wagtail project with lots of pages, and log entries.

Check http://127.0.0.1:9000/admin/reports/site-history/ with the project running Wagtail 5.2 - page will probably be slow to load.

(Note: I did try and create a quick script to test this with Wagtail's starter project - but the performance of SQLite and a lack of a debug toolbar slowing things down made it a bit tricky!).

-   I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: yes

### Technical details

-   Python version: 3.11 / any
-   Django version: 4.2 / any
-   Wagtail version: 5.2 / main
-   Browser version: n/a


Discussion:

Resolving patch:
diff --git a/wagtail/admin/views/reports/base.py b/wagtail/admin/views/reports/base.py
--- a/wagtail/admin/views/reports/base.py
+++ b/wagtail/admin/views/reports/base.py
@@ -17,8 +17,12 @@ def decorate_paginated_queryset(self, object_list):
 
     def get(self, request, *args, **kwargs):
         self.filters, self.object_list = self.get_filtered_queryset()
-        self.object_list = self.decorate_paginated_queryset(self.object_list)
         context = self.get_context_data()
+        # Decorate the queryset *after* Django's BaseListView has returned a paginated/reduced
+        # list of objects
+        context["object_list"] = self.decorate_paginated_queryset(
+            context["object_list"]
+        )
         return self.render_to_response(context)
 
     def get_context_data(self, *args, **kwargs):


Test patch:
diff --git a/wagtail/admin/tests/test_reports_views.py b/wagtail/admin/tests/test_reports_views.py
--- a/wagtail/admin/tests/test_reports_views.py
+++ b/wagtail/admin/tests/test_reports_views.py
@@ -1,5 +1,6 @@
 import datetime
 from io import BytesIO
+from unittest import mock
 
 from django.conf import settings
 from django.conf.locale import LANG_INFO
@@ -12,6 +13,7 @@
 from openpyxl import load_workbook
 
 from wagtail.admin.views.mixins import ExcelDateFormatter
+from wagtail.admin.views.reports.audit_logging import LogEntriesView
 from wagtail.models import GroupPagePermission, ModelLogEntry, Page, PageLogEntry
 from wagtail.test.testapp.models import Advert
 from wagtail.test.utils import WagtailTestUtils
@@ -445,6 +447,24 @@ def test_log_entry_with_null_content_type(self):
         response = self.get()
         self.assertContains(response, "Unknown content type")
 
+    def test_decorated_queryset(self):
+        # Ensure that decorate_paginated_queryset is only called with the queryset for the current
+        # page, instead of all objects over all pages.
+        with mock.patch.object(
+            LogEntriesView,
+            "decorate_paginated_queryset",
+            side_effect=LogEntriesView.decorate_paginated_queryset,
+            autospec=True,
+        ) as decorate_paginated_queryset, mock.patch.object(
+            LogEntriesView, "paginate_by", return_value=1
+        ):
+            response = self.get()
+            decorate_paginated_queryset.assert_called_once()
+            queryset = decorate_paginated_queryset.call_args.args[1]
+            self.assertEqual(queryset.count(), 1)
+
+        self.assertEqual(response.status_code, 200)
+
 
 @override_settings(
     USE_L10N=True,