perf: view_tryout, une requete par lot au lieu d une par ligne
PERF-001, la page la plus consultee de l application. Quatre boucles
posaient une requete par ligne :
User.query.get() par inscription
Evaluation.query par joueur inscrit, pour savoir si ce coach
l avait deja evalue
TeamMember.query par equipe
User.query.get() par membre d equipe
Plus, sur chaque match de type player_vs_player, deux interrogations
supplementaires de la relation dynamique `participants` pour trier par
camp -- alors que la liste complete venait d etre chargee douze lignes plus
haut.
Toutes remplacees par un chargement groupe. Les evaluations de ce coach
sont deduites de la liste `evaluations` deja en memoire, pas redemandees.
Mesure, sur un tryout de 10 inscrits, 2 equipes et 1 match :
34 requetes avant, 12 apres. Le test fixe un budget de 25, volontairement
large -- il ne peut que baisser, et il echoue sur le code d avant.
_users_by_id() est le helper partage par les trois chargements ; une ligne
absente est simplement absente du dictionnaire, ce que faisait deja un
get() renvoyant None.
Un second test verifie que les dix joueurs apparaissent toujours sur la
page : une requete groupee qui perd des lignes est le risque reel ici, pas
l erreur bruyante.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -231,3 +231,84 @@ class TestPendingEvaluations:
|
||||
db.session.commit()
|
||||
|
||||
assert self._pending(client) == 1
|
||||
|
||||
|
||||
class TestViewTryout:
|
||||
"""PERF-001 — the most-visited page in the application ran one query
|
||||
per registration, one per player evaluated, one per team, and one per
|
||||
team member. Thirty registrants and four teams put it past a hundred
|
||||
round trips for a single render."""
|
||||
|
||||
@pytest.fixture
|
||||
def populated_tryout(self, app, make_user):
|
||||
"""A tryout with ten registrants split across two teams."""
|
||||
from app.models import Match, MatchParticipant, Team, TeamMember
|
||||
|
||||
admin_id = make_user('admin')
|
||||
player_ids = [make_user('player') for _ in range(10)]
|
||||
|
||||
with app.app_context():
|
||||
tryout = Tryout(
|
||||
title='Spring', game='Valorant', date=date(2030, 3, 1), created_by=admin_id
|
||||
)
|
||||
db.session.add(tryout)
|
||||
db.session.flush()
|
||||
|
||||
for player_id in player_ids:
|
||||
db.session.add(TryoutRegistration(tryout_id=tryout.id, player_id=player_id))
|
||||
|
||||
for index in range(2):
|
||||
team = Team(tryout_id=tryout.id, name=f'Team {index}', created_by=admin_id)
|
||||
db.session.add(team)
|
||||
db.session.flush()
|
||||
for player_id in player_ids[index * 5 : (index + 1) * 5]:
|
||||
db.session.add(TeamMember(team_id=team.id, player_id=player_id))
|
||||
|
||||
match = Match(
|
||||
tryout_id=tryout.id,
|
||||
title='Scrim',
|
||||
date=date(2030, 3, 2),
|
||||
match_type='player_vs_player',
|
||||
created_by=admin_id,
|
||||
)
|
||||
db.session.add(match)
|
||||
db.session.flush()
|
||||
for side, player_id in enumerate(player_ids[:4]):
|
||||
db.session.add(
|
||||
MatchParticipant(
|
||||
match_id=match.id, player_id=player_id, team_side=(side % 2) + 1
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
return tryout.id, admin_id, player_ids
|
||||
|
||||
def test_the_page_still_shows_everyone(self, app, client, as_role, populated_tryout):
|
||||
tryout_id, _admin_id, player_ids = populated_tryout
|
||||
as_role('admin')
|
||||
|
||||
body = client.get(f'/tryouts/{tryout_id}').get_data(as_text=True)
|
||||
|
||||
with app.app_context():
|
||||
usernames = [db.session.get(User, pid).username for pid in player_ids]
|
||||
for username in usernames:
|
||||
assert username in body, f'{username} is missing from the page'
|
||||
|
||||
def test_the_cost_does_not_grow_with_the_squad(
|
||||
self, app, client, as_role, populated_tryout, count_queries
|
||||
):
|
||||
tryout_id, _admin_id, _player_ids = populated_tryout
|
||||
as_role('admin')
|
||||
|
||||
with app.app_context():
|
||||
counter = count_queries()
|
||||
try:
|
||||
response = client.get(f'/tryouts/{tryout_id}')
|
||||
finally:
|
||||
counter.stop()
|
||||
|
||||
assert response.status_code == 200
|
||||
# Ten registrants, two teams of five, one match of four. The old
|
||||
# shape spent more than thirty queries on those rows alone; the
|
||||
# budget here is deliberately loose but far below that, and it can
|
||||
# only be lowered.
|
||||
assert 1 <= counter.total <= 25, f'{counter.total} SELECTs to render one tryout'
|
||||
|
||||
Reference in New Issue
Block a user