Issue Paginator: don't attempt to count the Issues

Counting incurs looking at all records which is too expensive if you have e.g.
1_000_000 issues.

Note that we take a different approach than the one for Events (where we
count-with-timeout). Reason for switching:
https://sqlite.org/forum/forumpost/fa65709226

For Events we have a known count for the non-query case (denormalized/counted
value), so we preserve what we had there. For Issues the trouble of keeping
counts right for muted/etc. is not (currently) worth it.
This commit is contained in:
Klaas van Schelven
2025-05-06 10:13:06 +02:00
parent 392f5a30be
commit 3783661054
2 changed files with 39 additions and 9 deletions

View File

@@ -200,22 +200,22 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6 text-slate-200"><path fill-rule="evenodd" d="M9.78 4.22a.75.75 0 0 1 0 1.06L7.06 8l2.72 2.72a.75.75 0 1 1-1.06 1.06L5.47 8.53a.75.75 0 0 1 0-1.06l3.25-3.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" /></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6 text-slate-200"><path fill-rule="evenodd" d="M9.78 4.22a.75.75 0 0 1 0 1.06L7.06 8l2.72 2.72a.75.75 0 1 1-1.06 1.06L5.47 8.53a.75.75 0 0 1 0-1.06l3.25-3.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" /></svg>
{% endif %} {% endif %}
{% if page_obj.paginator.num_pages > 1 %} {% if page_obj.object_list|length > 0 %}
Issues {{ page_obj.start_index|intcomma }}{{ page_obj.end_index|intcomma }} of {{ page_obj.paginator.count|intcomma }} Issues {{ page_obj.start_index|intcomma }} {{ page_obj.end_index|intcomma }}
{% elif page_obj.paginator.count > 0 %} {% else %}
{{ page_obj.paginator.count|intcomma }} Issues {% if page_obj.number > 1 %}
Less than {{ page_obj.start_index }} Issues {# corresponds to the 1/250 case of having an exactly full page and navigating to an empty page after that #}
{% else %}
0 Issues
{% endif %}
{% endif %} {% endif %}
{% if page_obj.has_next %} {% if page_obj.has_next %}
<a href="?{% add_to_qs page=page_obj.next_page_number %}" class="inline-flex" title="Next page"> <a href="?{% add_to_qs page=page_obj.next_page_number %}" class="inline-flex" title="Next page">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg>
</a> </a>
<a href="?{% add_to_qs page=page_obj.paginator.num_pages %}" class="inline-flex" title="Last page">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6"><path fill-rule="evenodd" d="M12.78 7.595a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06l3.25 3.25Zm-8.25-3.25 3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06Z" clip-rule="evenodd" /></svg>
</a>
{% else %} {% else %}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6 text-slate-200"><path fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6 text-slate-200"><path fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-6 h-6 text-slate-200"><path fill-rule="evenodd" d="M12.78 7.595a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06l3.25 3.25Zm-8.25-3.25 3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06l2.72-2.72-2.72-2.72a.75.75 0 0 1 1.06-1.06Z" clip-rule="evenodd" /></svg>
{% endif %} {% endif %}
</div> </div>

View File

@@ -15,6 +15,7 @@ from django.http import Http404
from django.core.paginator import Paginator, Page from django.core.paginator import Paginator, Page
from django.db.utils import OperationalError from django.db.utils import OperationalError
from django.conf import settings from django.conf import settings
from django.utils.functional import cached_property
from sentry.utils.safe import get_path from sentry.utils.safe import get_path
from sentry_sdk_extensions import capture_or_log_exception from sentry_sdk_extensions import capture_or_log_exception
@@ -87,6 +88,35 @@ class KnownCountPaginator(EagerPaginator):
return self._count return self._count
class UncountablePage(Page):
"""The Page subclass to be used with UncountablePaginator."""
@cached_property
def has_next(self):
# hack that works 249/250 times: if the current page is full, we have a next page
return len(self.object_list) == self.paginator.per_page
@cached_property
def end_index(self):
return (self.paginator.per_page * (self.number - 1)) + len(self.object_list)
class UncountablePaginator(EagerPaginator):
"""optimization: counting is too expensive; to be used in a template w/o .count and .last"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def _get_page(self, *args, **kwargs):
object_list = args[0]
object_list = list(object_list)
return UncountablePage(object_list, *(args[1:]), **kwargs)
@property
def count(self):
return 1_000_000_000 # big enough to be bigger than what you can click through or store in the DB.
def _request_repr(parsed_data): def _request_repr(parsed_data):
if "request" not in parsed_data: if "request" not in parsed_data:
return "" return ""
@@ -268,7 +298,7 @@ def _issue_list_pt_2(request, project, state_filter, unapplied_issue_ids):
if request.GET.get("q"): if request.GET.get("q"):
issue_list = search_issues(project, issue_list, request.GET["q"]) issue_list = search_issues(project, issue_list, request.GET["q"])
paginator = EagerPaginator(issue_list, 250) paginator = UncountablePaginator(issue_list, 250)
page_number = request.GET.get("page") page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number) page_obj = paginator.get_page(page_number)