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>
{% endif %}
{% if page_obj.paginator.num_pages > 1 %}
Issues {{ page_obj.start_index|intcomma }}{{ page_obj.end_index|intcomma }} of {{ page_obj.paginator.count|intcomma }}
{% elif page_obj.paginator.count > 0 %}
{{ page_obj.paginator.count|intcomma }} Issues
{% if page_obj.object_list|length > 0 %}
Issues {{ page_obj.start_index|intcomma }} {{ page_obj.end_index|intcomma }}
{% else %}
{% 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 %}
{% if page_obj.has_next %}
<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>
</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 %}
<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 %}
</div>

View File

@@ -15,6 +15,7 @@ from django.http import Http404
from django.core.paginator import Paginator, Page
from django.db.utils import OperationalError
from django.conf import settings
from django.utils.functional import cached_property
from sentry.utils.safe import get_path
from sentry_sdk_extensions import capture_or_log_exception
@@ -87,6 +88,35 @@ class KnownCountPaginator(EagerPaginator):
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):
if "request" not in parsed_data:
return ""
@@ -268,7 +298,7 @@ def _issue_list_pt_2(request, project, state_filter, unapplied_issue_ids):
if request.GET.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_obj = paginator.get_page(page_number)