Drupal, Views et les dangers du clickodrome....

Je l'avoue bien volontier, j'ai mis du temps à reconnaître que CCK était un outil réellement indispensable. Il n'y a que les imbéciles qui ne changent pas d'avis dit le dicton. Mais s'il est un module qui continue après tout ce temps à me laisser comme une poule devant un couteau, c'est bien Views. Et encore, c'était avant d'étudier d'un peu plus près les performances de la bête...

Cas d'école

Raison de mon énervement passager, une vue prise "au hasard" parmi plus de 140 (ouch!) sur un projet qui souffre de quelques lenteurs... Un vue toute simple présentée sous la forme d'un bloc contenant une liste elle-même issue d'une pauvre requête sensée renvoyer 1 enregistrement avec comme critères un type du noeud, une date CCK dans le passé et un tri par date décroissante. Pas le Pérou donc, et pourtant il faut pas moins de 230ms à Views pour générer cette petite vue. Relativement lent donc au regard du travail effectué, ce sera donc le cas pratique que j'utiliserais pour comprendre ce qui se passe.

La requête SQL

Pour voir un peu la tête de la requête générée, il est possible soit d'utiliser l'aperçu de Views2, soit, plus simplement, de mettre des traces en sortie de db_query. Voilà ce que cela donne :

SELECT
 DISTINCT(node.nid) AS nid,
 node_data_field_contenu_1.field_contenu_1_nid AS node_data_field_contenu_1_field_contenu_1_nid,
 node.type AS node_type,
 node.vid AS node_vid,
 node_data_field_contenu_1.field_contenu_2_nid AS node_data_field_contenu_1_field_contenu_2_nid,
 node_data_field_contenu_1.field_contenu_3_nid AS node_data_field_contenu_1_field_contenu_3_nid,
 node_data_field_contenu_1.field_contenu_4_nid AS node_data_field_contenu_1_field_contenu_4_nid,
 node_data_field_contenu_1.field_contenu_5_nid AS node_data_field_contenu_1_field_contenu_5_nid,
 node_data_field_contenu_1.field_contenu_6_nid AS node_data_field_contenu_1_field_contenu_6_nid,
 node_data_field_date.field_date_value AS node_data_field_date_field_date_value
FROM node node
LEFT JOIN content_field_date node_data_field_date ON node.vid = node_data_field_date.vid
LEFT JOIN content_type_une node_data_field_contenu_1 ON node.vid = node_data_field_contenu_1.vid
WHERE
 ((node.type IN ('tagazok')) AND (node.STATUS <> 0))
 AND (DATE_FORMAT(STR_TO_DATE(node_data_field_date.field_date_value, '%Y-%m-%dT%T'), '%Y-%m-%d\\\\T%H:%i:%s') <= '2009-06-30T00:55:00')
ORDER BY node_data_field_date_field_date_value DESC
LIMIT 0,1
Requêtes à la Views

Avouons déjà que c'est tout de même très moche, et à ceux qui me dirait "mais c'est pas grave, on le voit pas", je répondrais "le code HTML de MS-Word aussi personne ne le voit, pourtant y'en a plein que ça défrise...".

Mais au delà de ces considérations esthétiques propres à chacun, voyons ce que nous avons dans notre besace à la Prévert:

  • Un aliasing inutile de tous les champs (jusqu'à un node node bien mignon)
  • Une condition de barbare pour sélectionner une simple date dans le passé.
  • Un recherche dans un ensemble d'une seule valeur (node.type in ('tagazok')).
  • La remontée d'une tripotée de champs sans intérêt alors que seuls les field_contenu_XXX étaient demandés.
  • Un Distinct qui fait joli avec le limit 0,1.

Nous ne pouvons pas faire grand chose sur la condition barbare, mais voyons à quoi ressemblerait un code SQL débarrassé du reste de joyeusetés. Il ne s'agit pas vraiment d'optimisation mais juste d'un petit nettoyage printanier.

SELECT b.* FROM node n
LEFT JOIN content_field_date a ON n.vid = a.vid
LEFT JOIN content_type_une b ON n.vid = b.vid
WHERE
 n.type='tagazok' AND
 n.STATUS <> 0 AND
 (DATE_FORMAT(STR_TO_DATE(a.field_date_value, '%Y-%m-%dT%T'), '%Y-%m-%d\\T%H:%i:%s') <= '2009-06-29T18:38:49')
ORDER BY a.field_date_value DESC
LIMIT 1
requête à la main

MySQL le fautif ?

Notre premier test consistera donc à voir si les performances diffèrent significativement entre des deux requêtes. Pour ce faire, j'ai simplement crée un script Drupal procédant à 1000 exécutions de chacune des deux requêtes :

RequêteItérationsTemps
Views10000.54
Manuelle10000.47

Déjà rien que là, on a gagne 13.3% de performances ce qui est assez inquiétant. En effet, d'un point de vue purement sémantique, les deux requêtes sont tout de même très proches l'une de l'autre, et les bases de données sont sensées gommer ce genre de détail à travers leur module d'optimisation. J'aurais donc tendance ici à aller taper sur l'optimisateur de requête de MySQL qui semble bien en deçà de ce que l'on a l'habitude de trouver sur une base Oracle ou même PostgreSQL.

Le choix des types de champ

Ceci étant dit, nous parlons là de 13% et cela n'explique pas que le rendu soit si lent. ce qui n'est rien en comparaison de ce que nous coûte la condition barbare. En effet, si l'on retire cette horreur, c'est un gain de 39% que nous obtenons...

Ici Views n'est qu'une victime d'un petite désastre vienant de CCK. En effet, pour gérer les dates dans les contenus, CCK implique l'utilisation du module Date API). Ce dernier lui fournit trois types de date possibles : Date, DateStamp et TimeDate. Chacun de ces types a sa propre représentation interne en base de données. Le type Date n'est rien d'autre qu'un VARCHAR2, DateStamp est un entier contenant un timestamp UNIX et DateTime correspond au type "date" du SGBD.

Du coup, en choisissant le type Date pour un champ, on oblige chaque requête devant implémenter une condition sur ce champ, à jongler entre la représentation textuelle de la date et sa représentation "entière". Des conversions qui ont un coût non négligeable à additionner à celui de devoir travailler sur un plus grand volume de données.

Le type "Date" pour un champ CCK est donc à éviter à tout prix car sinon au profit d'un DateStamp.

Le coût du click

Mais le plus intéressant n'est pas encore là. Pour avoir une vision complète, il nous faut comparer cette fois les vitesses de rendu du bloc views, celui impliqué par en gros 20 lignes de PHP dans un module.

Rendu de blocItérationsTempsselectdeleteinsertupdate
Par Views1002.3153101020
Par un module custom1000.07100000

Notez pour l'échelle de grandeur, que le premier teste (SQL) a été répété 1000 fois pour obtenir ces temps, et seulement 10 fois pour ceux portant sur la génération des blocs. Et là nous sommes juste à 96.4% de performances supplémentaire en faveur de nos 20 lignes de PHP.

Alors pourquoi ? Une partie de la réponse se trouve dans les colonnes suivantes. Disons que pour une simple liste générer 5.3 requêtes SELECT, 1 requête DELETE, 2 requêtes UPDATE et 1 requêtes INSERT, ne peut pas, avec la meilleur bonne volonté du monde, donner de bons résultats.

C'est un peu la quadrature du cercles cette histoire, il faut des requêtes pour remonter les modèles de vues stockées en base, qui vont permettre à grand coups de hooks et de plugins (et donc de temps CPU), de générer une requêtes SQL que l'on va, à grand coups de hooks et de plugins formater en une simple liste à puces. Et comme tout ceci prend du temps, on rajoute en plus des couches de cache pour cacher la misère (les INSERT/DELETE/UPDATE). Tout cela pour s'éviter de rédiger 20 lignes de PHP, c'est un peu dur.

Le cache de block ne sauvera personne

Après j'en entend qui disent "pas grave, y'a le cache de blocks, et puis si c'est pas suffisant, y'a le cache de page". Certes, mais déjà le cache de page ne fonctionne que pour les visiteurs anonymes et celui de block fait ce qu'il peut avec les authentifiés.

Ensuite les caches de bloc ET de page sont virés dès qu'un contenu et/ou commentaire est ajouté. Autant dire que sur un site à gros trafic il ne faut pas trop y compter. Si si, je vous assure... Ligne 776 de comments.module :

// Clear the cache so an anonymous user can see his comment being added.
cache_clear_all();

Et si l'on regarde la fonction cache_clear_all() :

if (!isset($cid) && !isset($table)) {
  // Clear the block cache first, so stale data will
  // not end up in the page cache.
  cache_clear_all(NULL, 'cache_block');
  cache_clear_all(NULL, 'cache_page');
  return;
}

Et voilà...

Conclusion

Autant Views est clairement plus lent qu'un travail fait proprement à la main, autant il ne faut pas non plus jeter le bébé avec l'eau du bain. Pour de petits sites tranquilles ce module permet à un non-développeur de fabriquer sans connaissance particulières ses propres listes, flux, blocs, etc... Maintenant pour un site "pro", il faut clairement réfléchir un peu avant de coller cela sur une home-page.

Pour le reste, Views est une redoutable documentation interactive permettant en quelques clicks de comprendre la construction de requêtes un peu tordues du genre "comment avoir la liste des contenus ayant les meilleurs notes et écrits par mes amis". Rien que pour cela je lui suis redevable :-)

Vus : 521
Publié par arNuméral : 54