La release v15.6...

a marqué ce sujet comme résolu.

Je pense avoir découvert une des causes non négligeables de la lenteur de la page d'index des forums.

Regardons le templates forum/includes/forum.part.html, à la ligne 30 nous avons un

1
<a href="{{ last_message.topic.last_read_post.get_absolute_url }}">

ce qui génère en cascade :

  • l'appel de last_message.topic.last_read_post (parce que, "ouf" topic a été chargé)
  • l'appel de get_absolute_url

Seulement voilà

  • last_read_post fait ceci :
1
2
3
4
5
6
7
8
try:
            return TopicRead.objects \
                            .select_related() \
                            .filter(topic__pk=self.pk,
                                    user__pk=get_current_user().pk) \
                            .latest('post__position').post
        except TopicRead.DoesNotExist:
            return self.first_post()

qui génère dans le meilleur des cas (quand il n'y a pas besoin d'appeler first_post) : une requête qui précharge tous les attributs de topic_read MAIS PAS CEUX DE post et qui ressemble à ça (je l'ai formatté pour vous):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
SELECT forum_topicread.id, forum_topicread.topic_id, forum_topicread.post_id, forum_topicread.user_id,
       forum_topic.id, forum_topic.title, forum_topic.subtitle, forum_topic.forum_id,
       forum_topic.author_id, forum_topic.last_message_id, forum_topic.pubdate, forum_topic.is_solved,
       forum_topic.is_locked, forum_topic.is_sticky, forum_topic.key,
       forum_forum.id, forum_forum.title, forum_forum.subtitle, forum_forum.image,  
       forum_forum.category_id, forum_forum.position_in_category, forum_forum.slug,
       forum_category.id, forum_category.title, forum_category.position, forum_category.slug,
       T6.id, T6.password, T6.last_login, T6.is_superuser, T6.username, T6.first_name,
       T6.last_name, T6.email, T6.is_staff, T6.is_active, T6.date_joined, utils_comment.id,
       utils_comment.author_id, utils_comment.editor_id, utils_comment.ip_address,
       utils_comment.position, utils_comment.text, utils_comment.text_html, utils_comment.like,
       utils_comment.dislike, utils_comment.pubdate, utils_comment.update,
       utils_comment.is_visible, utils_comment.text_hidden,
       forum_post.comment_ptr_id, forum_post.topic_id, forum_post.is_useful,
       T9.id, T9.password, T9.last_login, T9.is_superuser, T9.username, T9.first_name,
       T9.last_name, T9.email, T9.is_staff, T9.is_active, T9.date_joined,
       T10.id, T10.title, T10.subtitle, T10.forum_id, T10.author_id, T10.last_message_id,
       T10.pubdate, T10.is_solved, T10.is_locked, T10.is_sticky, T10.key,
       T11.id, T11.title, T11.subtitle, T11.image, T11.category_id, T11.position_in_category,
       T11.slug,
       T12.id, T12.title, T12.position, T12.slug,
       T13.id, T13.password, T13.last_login, T13.is_superuser, T13.username, T13.first_name,
       T13.last_name, T13.email, T13.is_staff, T13.is_active, T13.date_joined,
       auth_user.id, auth_user.password, auth_user.last_login, auth_user.is_superuser,
       auth_user.username, auth_user.first_name, auth_user.last_name, auth_user.email,
       auth_user.is_staff, auth_user.is_active, auth_user.date_joined
FROM forum_topicread
INNER JOIN auth_user ON ( forum_topicread.user_id = auth_user.id )
INNER JOIN forum_topic ON ( forum_topicread.topic_id = forum_topic.id )
INNER JOIN forum_forum ON ( forum_topic.forum_id = forum_forum.id )
INNER JOIN forum_category ON ( forum_forum.category_id = forum_category.id )
INNER JOIN auth_user T6 ON ( forum_topic.author_id = T6.id )
INNER JOIN forum_post ON ( forum_topicread.post_id = forum_post.comment_ptr_id )
INNER JOIN utils_comment ON ( forum_post.comment_ptr_id = utils_comment.id )
INNER JOIN auth_user T9 ON ( utils_comment.author_id = T9.id )
INNER JOIN forum_topic T10 ON ( forum_post.topic_id = T10.id )
INNER JOIN forum_forum T11 ON ( T10.forum_id = T11.id )
INNER JOIN forum_category T12 ON ( T11.category_id = T12.id )
INNER JOIN auth_user T13 ON ( T10.author_id = T13.id )
WHERE (forum_topicread.user_id IS NULL AND forum_topicread.topic_id = %s)
ORDER BY utils_comment.position DESC LIMIT 1

Vous vous dites peut être "Putain de requête pour ne retrouver qu'une simple URL"? Mais c'est pas fini!

car comme je vous le disais, le chargement étant trop complexe, l'ORM, ne sait rien faire de ça ! Alors il ne préchargement le topic lié au post. du coup quand on fait notre get_absolute_url on a le droit à une nouvelle requête pour sélectionner les topics :

1
SELECT forum_topic.id, forum_topic.title, forum_topic.subtitle, forum_topic.forum_id, forum_topic.author_id, forum_topic.last_message_id, forum_topic.pubdate, forum_topic.is_solved, forum_topic.is_locked, forum_topic.is_sticky, forum_topic.key FROM forum_topic WHERE forum_topic.id = %s LIMIT 21

Cette seconde semble plus petite, on se dit que c'est cool. Mais n'oubliez pas qu'on fait ces deux requêtes à chaque forum (sur la page d'accueil) ou topic (quand on est dans une catégorie) JUSTE POUR OBTENIR UNE PUTAIN D'URL!

Et attendez, vous avez pas le meilleur : quand on est en anonyme (c'est ce qui est mesuré par munin), la première requête froire FORCEMENT! Bah oui, les utilisateurs non connecté n'ont pas de "topic_read", donc la requête est exécuté, elle balance un exception, qui doit être attrapée, pour ensuite lancer le first_post() ! Qui lui même lance une belle requête :

1
2
3
4
5
6
SELECT utils_comment.id, utils_comment.author_id, utils_comment.editor_id, utils_comment.ip_address, utils_comment.position, utils_comment.text, utils_comment.text_html, utils_comment.like, utils_comment.dislike, utils_comment.pubdate, utils_comment.update, utils_comment.is_visible, utils_comment.text_hidden, forum_post.comment_ptr_id, forum_post.topic_id, forum_post.is_useful, auth_user.id, auth_user.password, auth_user.last_login, auth_user.is_superuser, auth_user.username, auth_user.first_name, auth_user.last_name, auth_user.email, auth_user.is_staff, auth_user.is_active, auth_user.date_joined 
FROM forum_post 
INNER JOIN utils_comment ON ( forum_post.comment_ptr_id = utils_comment.id )
INNER JOIN auth_user ON ( utils_comment.author_id = auth_user.id )
WHERE forum_post.topic_id = %s
ORDER BY utils_comment.position ASC LIMIT 1

Ce qui nous génère, rien que pour ce phénomène 36 requêtes foncièrement surchargées (on load plein de truc tels que les auteurs…) en prod pour un anonyme, juste pour donner 12 putain d'urls.

Vous serez sûrement heureux d'apprendre que j'ai totalement alléger nos requêtes, j'ai aussi précharché les comptes… ce qui fait que sur mon instance locale je suis passé de

64 requêtes en 18ms à 28 requêtes en 10ms

Je lance la PR.

Il y a sûrement quelques choses à améliorer sur cette page pour éviter de charger trop de choses, mais mes 28 requêtes sont assez légères désormais et qui dit 28 requêtes dit moins de temps à attendre dans les dialogues avec la bdd.

tu veux dire sur mon instance locale?

3 catégories 12 forums 4 topics 5 messages

Si on augmente juste le nombre de topics et de messages, ça donne :

40 topics

passage de 93 requêtes en 19-22ms(peu stable) (dont l'immense répétée 12 fois) à 45 requêtes (10-11 ms) plus du tout immense. pour les anonymes

Pour les connectés (admin, pas d'alerte, 2 notifs), je passe de 122 requête (25-30ms pas stable) à 74 requêtes (14-15ms).

+0 -0

Joli travail.

Toujours compliqué de donner des temps avec les BDDs, il faut benchmarker dans des situations défavorables, etc. Mais réduire d'autant (tout en les simplifiant) le nb. de requêtes sur une page aussi visitée ne peut être que bénéfique.

On est en plein dans le double tranchant des ORM, une simplicité apparente dans l'écriture mais souvent des trucs très peu désirables bien planqués derrière des accesseurs…

Bien joue.

+2 -0

On est en plein dans le double tranchant des ORM, une simplicité apparente dans l'écriture mais souvent des trucs très peu désirables bien planqués derrière des accesseurs…

en fait c'est pire que ça : y'a eu une personne qui soit par fatigue soit par tâtonnement a juste mis un select_related() qui a pour effet de sélectionner les trucs en cascade de partout. C'est ça qui faisait tout foirer.

Ensuite, il y a nos accesseurs qui ont tendance à faire des trucs qui ne sont pas triviaux et qui balancent des requêtes alors que ce ne sont que des accesseurs.

Malheureusement, dans une PR comme la mienne je ne peux pas changer fondamentallement le fonctionnement d'où l'ajout de paramètres optionnels ou de disjonction.

Le jour où notre problème sera "last_read_post" et "first_post" on pourra commencer à optimiser ça, pour l'instant, c'est pas le cas.

Juste parce que j'arrive pas à les passer sur gh

5.

id

select_type

table

type

possible_keys

key

key_len

ref

rows

Extra

1 1

SIMPLE SIMPLE

forum_post utils_comment

index eq_ref

PRIMARY PRIMARY,utils_comment_4757fe07

PRIMARY PRIMARY

4 4

NULL zdsdb.forum_post.comment_ptr_id

26 1

Using where

id

select_type

table

type

possible_keys

key

key_len

ref

rows

Extra

1 1 1 2 2

PRIMARY PRIMARY PRIMARY MATERIALIZED MATERIALIZED

forum_category forum_forum forum_forum_group U1 U0

ALL ref ref ref eq_ref

PRIMARY forum_forum_b583a629 forum_id,forum_forum_group_19bc3ff1 user_id,auth_user_groups_e8701ad4,auth_user_groups_0e939a4f PRIMARY

NULL forum_forum_b583a629 forum_id user_id PRIMARY

NULL 4 4 4 4

NULL zdsdb.forum_category.id zdsdb.forum_forum.id const zdsdb.U1.group_id

4 1 1 1 1

Using temporary; Using filesort

Using where; Using index; Distinct Using index Using index

8.

id

select_type

table

type

possible_keys

key

key_len

ref

rows

Extra

1

SIMPLE

auth_user

ALL

NULL

NULL

NULL

NULL

7

Using where

9.

id

select_type

table

type

possible_keys

key

key_len

ref

rows

Extra

1 1 1

SIMPLE SIMPLE SIMPLE

utils_alert utils_comment auth_user

ALL eq_ref eq_ref

utils_alert_4f331e2f,utils_alert_69b97d17 PRIMARY PRIMARY

NULL PRIMARY PRIMARY

NULL 4 4

NULL zdsdb.utils_alert.comment_id zdsdb.utils_alert.author_id

1 1 1

Using filesort

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte