Skip to content

Commit

Permalink
perf: speed up user profile page (pypi#17269)
Browse files Browse the repository at this point in the history
* perf: speed up user profile page

For users with a large amount of projects, the template iteration through
`projects` to fetch their latest `release` details turns into an n+1
query, and can time out on larger user profile pages.

Refactor the query to perform most of the logic to fetch only the data
necessary to render the template correctly, removing the need to load
full entities.

Signed-off-by: Mike Fiedler <miketheman@gmail.com>

* make translations

Signed-off-by: Mike Fiedler <miketheman@gmail.com>

---------

Signed-off-by: Mike Fiedler <miketheman@gmail.com>
  • Loading branch information
miketheman authored Dec 11, 2024
1 parent 6411beb commit 8488a4b
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 60 deletions.
37 changes: 36 additions & 1 deletion tests/unit/accounts/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@
OrganizationInvitationFactory,
OrganizationRoleFactory,
)
from ...common.db.packaging import ProjectFactory, RoleFactory, RoleInvitationFactory
from ...common.db.packaging import (
ProjectFactory,
ReleaseFactory,
RoleFactory,
RoleInvitationFactory,
)


class TestFailedLoginView:
Expand Down Expand Up @@ -146,6 +151,36 @@ def test_returns_user(self, db_request):
user = UserFactory.create()
assert views.profile(user, db_request) == {"user": user, "projects": []}

def test_user_profile_queries_once_for_all_projects(
self, db_request, query_recorder
):
user = UserFactory.create()
projects = ProjectFactory.create_batch(3)
for project in projects:
# associate the user to each project as role: owner
RoleFactory.create(user=user, project=project)
# Add some releases, with time skew to ensure the ordering is correct
ReleaseFactory.create(
project=project, created=project.created + datetime.timedelta(minutes=1)
)
ReleaseFactory.create(
project=project, created=project.created + datetime.timedelta(minutes=2)
)
# Add a prerelease, shouldn't affect any results
ReleaseFactory.create(
project=project,
created=project.created + datetime.timedelta(minutes=3),
is_prerelease=True,
)

with query_recorder:
response = views.profile(user, db_request)

assert response["user"] == user
assert len(response["projects"]) == 3
# Two queries, one for the user (via context), one for their projects
assert len(query_recorder.queries) == 2


class TestAccountsSearch:
def test_unauthenticated_raises_401(self):
Expand Down
50 changes: 45 additions & 5 deletions warehouse/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from pyramid.interfaces import ISecurityPolicy
from pyramid.security import forget, remember
from pyramid.view import view_config, view_defaults
from sqlalchemy import and_, func, select
from sqlalchemy.exc import NoResultFound
from webauthn.helpers import bytes_to_base64url
from webob.multidict import MultiDict
Expand Down Expand Up @@ -168,14 +169,53 @@ def profile(user, request):
if user.username != request.matchdict.get("username", user.username):
return HTTPMovedPermanently(request.current_route_path(username=user.username))

projects = (
request.db.query(Project)
.filter(Project.users.contains(user))
.join(Project.releases)
# Query only for the necessary data that the template needs
# Subquery to get the latest release date for each project associated with the user
latest_releases_subquery = (
select(
Release.project_id, func.max(Release.created).label("latest_release_date")
)
.join(Role, Release.project_id == Role.project_id)
.where(Role.user_id == user.id)
.group_by(Release.project_id)
.subquery()
)
# Main query to select the latest releases
stmt = (
select(
Project.name,
Project.normalized_name,
Release.created,
Release.summary,
)
.join(Role, Project.id == Role.project_id)
.outerjoin(
latest_releases_subquery,
Project.id == latest_releases_subquery.c.project_id,
)
.outerjoin(
Release,
and_(
Release.project_id == latest_releases_subquery.c.project_id,
Release.created == latest_releases_subquery.c.latest_release_date,
),
)
.where(Role.user_id == user.id)
.distinct()
.order_by(Release.created.desc())
.all()
)

# Construct the list of projects with their latest releases from query results
projects = [
{
"name": row.name,
"normalized_name": row.normalized_name,
"created": row.created,
"summary": row.summary,
}
for row in request.db.execute(stmt)
]

return {"user": user, "projects": projects}


Expand Down
104 changes: 52 additions & 52 deletions warehouse/locale/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -119,194 +119,194 @@ msgstr ""
msgid "The username isn't valid. Try again."
msgstr ""

#: warehouse/accounts/views.py:119
#: warehouse/accounts/views.py:120
msgid ""
"There have been too many unsuccessful login attempts. You have been "
"locked out for {}. Please try again later."
msgstr ""

#: warehouse/accounts/views.py:136
#: warehouse/accounts/views.py:137
msgid ""
"Too many emails have been added to this account without verifying them. "
"Check your inbox and follow the verification links. (IP: ${ip})"
msgstr ""

#: warehouse/accounts/views.py:148
#: warehouse/accounts/views.py:149
msgid ""
"Too many password resets have been requested for this account without "
"completing them. Check your inbox and follow the verification links. (IP:"
" ${ip})"
msgstr ""

#: warehouse/accounts/views.py:330 warehouse/accounts/views.py:399
#: warehouse/accounts/views.py:401 warehouse/accounts/views.py:430
#: warehouse/accounts/views.py:432 warehouse/accounts/views.py:538
#: warehouse/accounts/views.py:370 warehouse/accounts/views.py:439
#: warehouse/accounts/views.py:441 warehouse/accounts/views.py:470
#: warehouse/accounts/views.py:472 warehouse/accounts/views.py:578
msgid "Invalid or expired two factor login."
msgstr ""

#: warehouse/accounts/views.py:393
#: warehouse/accounts/views.py:433
msgid "Already authenticated"
msgstr ""

#: warehouse/accounts/views.py:473
#: warehouse/accounts/views.py:513
msgid "Successful WebAuthn assertion"
msgstr ""

#: warehouse/accounts/views.py:569 warehouse/manage/views/__init__.py:873
#: warehouse/accounts/views.py:609 warehouse/manage/views/__init__.py:873
msgid "Recovery code accepted. The supplied code cannot be used again."
msgstr ""

#: warehouse/accounts/views.py:661
#: warehouse/accounts/views.py:701
msgid ""
"New user registration temporarily disabled. See https://pypi.org/help"
"#admin-intervention for details."
msgstr ""

#: warehouse/accounts/views.py:799
#: warehouse/accounts/views.py:839
msgid "Expired token: request a new password reset link"
msgstr ""

#: warehouse/accounts/views.py:801
#: warehouse/accounts/views.py:841
msgid "Invalid token: request a new password reset link"
msgstr ""

#: warehouse/accounts/views.py:803 warehouse/accounts/views.py:904
#: warehouse/accounts/views.py:1008 warehouse/accounts/views.py:1177
#: warehouse/accounts/views.py:843 warehouse/accounts/views.py:944
#: warehouse/accounts/views.py:1048 warehouse/accounts/views.py:1217
msgid "Invalid token: no token supplied"
msgstr ""

#: warehouse/accounts/views.py:807
#: warehouse/accounts/views.py:847
msgid "Invalid token: not a password reset token"
msgstr ""

#: warehouse/accounts/views.py:812
#: warehouse/accounts/views.py:852
msgid "Invalid token: user not found"
msgstr ""

#: warehouse/accounts/views.py:822
#: warehouse/accounts/views.py:862
msgid "Invalid token: user has logged in since this token was requested"
msgstr ""

#: warehouse/accounts/views.py:840
#: warehouse/accounts/views.py:880
msgid ""
"Invalid token: password has already been changed since this token was "
"requested"
msgstr ""

#: warehouse/accounts/views.py:872
#: warehouse/accounts/views.py:912
msgid "You have reset your password"
msgstr ""

#: warehouse/accounts/views.py:900
#: warehouse/accounts/views.py:940
msgid "Expired token: request a new email verification link"
msgstr ""

#: warehouse/accounts/views.py:902
#: warehouse/accounts/views.py:942
msgid "Invalid token: request a new email verification link"
msgstr ""

#: warehouse/accounts/views.py:908
#: warehouse/accounts/views.py:948
msgid "Invalid token: not an email verification token"
msgstr ""

#: warehouse/accounts/views.py:917
#: warehouse/accounts/views.py:957
msgid "Email not found"
msgstr ""

#: warehouse/accounts/views.py:920
#: warehouse/accounts/views.py:960
msgid "Email already verified"
msgstr ""

#: warehouse/accounts/views.py:937
#: warehouse/accounts/views.py:977
msgid "You can now set this email as your primary address"
msgstr ""

#: warehouse/accounts/views.py:941
#: warehouse/accounts/views.py:981
msgid "This is your primary address"
msgstr ""

#: warehouse/accounts/views.py:946
#: warehouse/accounts/views.py:986
msgid "Email address ${email_address} verified. ${confirm_message}."
msgstr ""

#: warehouse/accounts/views.py:1004
#: warehouse/accounts/views.py:1044
msgid "Expired token: request a new organization invitation"
msgstr ""

#: warehouse/accounts/views.py:1006
#: warehouse/accounts/views.py:1046
msgid "Invalid token: request a new organization invitation"
msgstr ""

#: warehouse/accounts/views.py:1012
#: warehouse/accounts/views.py:1052
msgid "Invalid token: not an organization invitation token"
msgstr ""

#: warehouse/accounts/views.py:1016
#: warehouse/accounts/views.py:1056
msgid "Organization invitation is not valid."
msgstr ""

#: warehouse/accounts/views.py:1025
#: warehouse/accounts/views.py:1065
msgid "Organization invitation no longer exists."
msgstr ""

#: warehouse/accounts/views.py:1076
#: warehouse/accounts/views.py:1116
msgid "Invitation for '${organization_name}' is declined."
msgstr ""

#: warehouse/accounts/views.py:1139
#: warehouse/accounts/views.py:1179
msgid "You are now ${role} of the '${organization_name}' organization."
msgstr ""

#: warehouse/accounts/views.py:1173
#: warehouse/accounts/views.py:1213
msgid "Expired token: request a new project role invitation"
msgstr ""

#: warehouse/accounts/views.py:1175
#: warehouse/accounts/views.py:1215
msgid "Invalid token: request a new project role invitation"
msgstr ""

#: warehouse/accounts/views.py:1181
#: warehouse/accounts/views.py:1221
msgid "Invalid token: not a collaboration invitation token"
msgstr ""

#: warehouse/accounts/views.py:1185
#: warehouse/accounts/views.py:1225
msgid "Role invitation is not valid."
msgstr ""

#: warehouse/accounts/views.py:1200
#: warehouse/accounts/views.py:1240
msgid "Role invitation no longer exists."
msgstr ""

#: warehouse/accounts/views.py:1231
#: warehouse/accounts/views.py:1271
msgid "Invitation for '${project_name}' is declined."
msgstr ""

#: warehouse/accounts/views.py:1297
#: warehouse/accounts/views.py:1337
msgid "You are now ${role} of the '${project_name}' project."
msgstr ""

#: warehouse/accounts/views.py:1548 warehouse/accounts/views.py:1791
#: warehouse/accounts/views.py:1588 warehouse/accounts/views.py:1831
#: warehouse/manage/views/__init__.py:1409
msgid ""
"Trusted publishing is temporarily disabled. See https://pypi.org/help"
"#admin-intervention for details."
msgstr ""

#: warehouse/accounts/views.py:1569
#: warehouse/accounts/views.py:1609
msgid "disabled. See https://pypi.org/help#admin-intervention for details."
msgstr ""

#: warehouse/accounts/views.py:1585
#: warehouse/accounts/views.py:1625
msgid ""
"You must have a verified email in order to register a pending trusted "
"publisher. See https://pypi.org/help#openid-connect for details."
msgstr ""

#: warehouse/accounts/views.py:1598
#: warehouse/accounts/views.py:1638
msgid "You can't register more than 3 pending trusted publishers at once."
msgstr ""

#: warehouse/accounts/views.py:1614 warehouse/manage/views/__init__.py:1464
#: warehouse/accounts/views.py:1654 warehouse/manage/views/__init__.py:1464
#: warehouse/manage/views/__init__.py:1577
#: warehouse/manage/views/__init__.py:1689
#: warehouse/manage/views/__init__.py:1799
Expand All @@ -315,29 +315,29 @@ msgid ""
"again later."
msgstr ""

#: warehouse/accounts/views.py:1625 warehouse/manage/views/__init__.py:1478
#: warehouse/accounts/views.py:1665 warehouse/manage/views/__init__.py:1478
#: warehouse/manage/views/__init__.py:1591
#: warehouse/manage/views/__init__.py:1703
#: warehouse/manage/views/__init__.py:1813
msgid "The trusted publisher could not be registered"
msgstr ""

#: warehouse/accounts/views.py:1639
#: warehouse/accounts/views.py:1679
msgid ""
"This trusted publisher has already been registered. Please contact PyPI's"
" admins if this wasn't intentional."
msgstr ""

#: warehouse/accounts/views.py:1666
#: warehouse/accounts/views.py:1706
msgid "Registered a new pending publisher to create "
msgstr ""

#: warehouse/accounts/views.py:1805 warehouse/accounts/views.py:1818
#: warehouse/accounts/views.py:1825
#: warehouse/accounts/views.py:1845 warehouse/accounts/views.py:1858
#: warehouse/accounts/views.py:1865
msgid "Invalid publisher ID"
msgstr ""

#: warehouse/accounts/views.py:1831
#: warehouse/accounts/views.py:1871
msgid "Removed trusted publisher for project "
msgstr ""

Expand Down
Loading

0 comments on commit 8488a4b

Please sign in to comment.