Skip to content

Dashboard: don't prefetch latest build #12400

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 14, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions readthedocs/projects/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Project models."""

import fnmatch
from functools import lru_cache
import hashlib
import hmac
import os
Expand Down Expand Up @@ -645,9 +646,6 @@ class Project(models.Model):
blank=True,
)

# Property used for storing the latest build for a project when prefetching
LATEST_BUILD_CACHE = "_latest_build"

class Meta:
ordering = ("slug",)
verbose_name = _("project")
Expand Down Expand Up @@ -1100,23 +1098,17 @@ def full_find(self, filename, version):
matches.append(os.path.join(root, match))
return matches

@lru_cache(maxsize=1)
def get_latest_build(self, finished=True):
"""
Get latest build for project.

:param finished: Return only builds that are in a finished state
"""
# Check if there is `_latest_build` attribute in the Queryset.
# Used for Database optimization.
if hasattr(self, self.LATEST_BUILD_CACHE):
if self._latest_build:
return self._latest_build[0]
return None

kwargs = {"type": "html"}
kwargs = {}
if finished:
kwargs["state"] = "finished"
return self.builds(manager=INTERNAL).filter(**kwargs).first()
return self.builds(manager=INTERNAL).filter(**kwargs).select_related("version").first()

def active_versions(self):
from readthedocs.builds.models import Version
Expand Down
20 changes: 10 additions & 10 deletions readthedocs/projects/querysets.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from django.db.models import Count
from django.db.models import Exists
from django.db.models import OuterRef
from django.db.models import Prefetch
from django.db.models import Q

from readthedocs.core.permissions import AdminPermission
Expand Down Expand Up @@ -134,25 +133,26 @@ def max_concurrent_builds(self, project):

def prefetch_latest_build(self):
"""
Prefetch "latest build" for each project.
Prefetch and annotate to avoid N+1 queries.

.. note::

This should come after any filtering.
"""
from readthedocs.builds.models import Build

# Prefetch the latest build for each project.
latest_build = Prefetch(
"builds",
Build.internal.select_related("version").order_by("-date")[:1],
to_attr=self.model.LATEST_BUILD_CACHE,
)
query = self.prefetch_related(latest_build)
# NOTE: prefetching the latest build will perform worse than just
# accessing the latest build for each project.
# While prefetching reduces the number of queries,
# the query used to fetch the latest build can be quite expensive,
# specially in projects with lots of builds.
# Not prefetching here is fine, as this query is paginated by 15
# items per page, so it will generate at most 15 queries.

# This annotation performs fine in all cases.
# Annotate whether the project has a successful build or not,
# to avoid N+1 queries when showing the build status.
return query.annotate(
return self.annotate(
_has_good_build=Exists(Build.internal.filter(project=OuterRef("pk"), success=True))
)

Expand Down
2 changes: 1 addition & 1 deletion readthedocs/rtd_tests/tests/test_project_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ def test_dashboard_number_of_queries(self):
state=BUILD_STATE_FINISHED,
)

with self.assertNumQueries(12):
with self.assertNumQueries(27):
r = self.client.get(reverse(("projects_dashboard")))
assert r.status_code == 200

Expand Down