XHTML.net

Technology talks by Loïc d’Anterroches

News, articles, PHP, scripts, XHTML/CSS, …

  1. Home
  2. PHP: Hypertext Preprocessor
  3. Pluf - Framework en PHP5

Un serveur de chat temps réel en PHP en 30 lines de code avec Photon

The 2011-02-25 at 17:14 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.


Le code complet du serveur:

    public function chatbox($request, $match)
{
static $user_list = array();
$data = $request->BODY;
if ('join' === $request->BODY->type) {
$request->conn->deliver($request->mess->sender,
array_keys($user_list),
json_encode($request->BODY));
$user_list[$request->mess->conn_id] = $request->BODY->user;
$res = array('type' => 'userList',
'users' => array_values($user_list));
print "JOIN ". $request->mess->conn_id . "\n";
return new \photon\http\response\Json($res);
} elseif ('disconnect' === $request->BODY->type) {
print "DISCONNECTED ". $request->mess->conn_id . "\n";

if (isset($user_list[$request->mess->conn_id])) {
$data->user = $user_list[$request->mess->conn_id];
unset($user_list[$request->mess->conn_id]);
}
$request->conn->deliver($request->mess->sender,
array_keys($user_list),
json_encode($data));
} elseif (!isset($user_list[$request->mess->conn_id])) {
$user_list[$data->user] = $request->mess->conn_id;
print "AUTO JOIN ". $request->mess->conn_id . "\n";
} elseif ('msg' === $data->type) {
$request->conn->deliver($request->mess->sender,
array_keys($user_list),
json_encode($data));
print "MESS FROM ". $request->mess->conn_id . "\n";
}
return false; // By default, say nothing
}

Je dois encore faire quelques amélioration pour ne pas avoir besoin d’accéder à $request->mess et avoir des raccourcis pour envoyer du JSON, mais bon. Il vous faut combien de lignes pour coder cela avec votre framework PHP ?

Photon, 10 fois plus rapide que Zend 1.1 et 2,7 fois plus rapide que Symfony 2

The 2011-02-22 at 08:01 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

Maintenant que Photon se trouve proche de la sortie officielle et que la boucle de dispatch a trouvé sa version finale, c’est à dire avec la gestion des middleware, du pré et post processing par vue, je me suis dit qu’un petit benchmark serait intéressant. J’ai repris pour cela le travail fait par Fabien Potencier avec le test de Symfony 2.

Photon atteint 90% de la vitesse de base de PHP sur le Hello World avec mod_php.

Framework            |      rel | Visuellement 
-------------------- | -------- | -----------------------------------------
photon-dev           |   0.9041 | ########################################
symfony-2.0.0alpha1  |   0.3312 | ##############
solar-1.0.0beta3     |   0.2825 | ############
lithium-0.6          |   0.2128 | #########
yii-1.1.1            |   0.1901 | ########
symfony-1.4.2        |   0.1737 | ########
zend-1.10            |   0.0906 | ####
cakephp-1.2.6        |   0.0513 | ##
flow3-1.0.0alpha7    |   0.0048 | 

En chiffres, cela donne :

  • 2.7 fois plus rapide que Symfony 2.0.0alpha1
  • 3.2 fois plus rapide que Solar 1.0.0beta3
  • 4 fois plus rapide que Lithium 0.6
  • 4.7 fois plus rapide que Yii 1.1.1
  • 5.2 fois plus rapide que symfony 1.4.2
  • 10 fois plus rapide que Zend 1.10
  • 18 fois plus rapide que CakePHP 1.2.6
  • 180 fois plus rapide que Flow3 1.0.0alpha7

La chose très intéressante est aussi que les performances ne se dégradent absolument pas de 4 à 100 connexions simultanées. ZeroMQ joue parfaitement son rôle de queue. Finalement vous avez toute la puissance d’un framework moderne (requêtes asynchrones, long polling, websocket, jsSocket, etc.) avec les performances de PHP tout nu. Par ailleurs, il ne faut pas oublier que Mongrel2 n’est pas encore optimisé au niveau performances, comme Photon supporte plus de 3500 req/s en direct via ZeroMQ, cela veut dire que toutes les améliorations de Mongrel2 seront prises, il devrait donc être possible d’arriver à des performances égales voir supérieures à mod_php.

$ siege -b -c4 -t5S http://localhost/hello.html
** SIEGE 2.68
** Preparing 4 concurrent users for battle.
The server is now under siege...
Lifting the server siege..      done.
Transactions:                  13913 hits
Availability:                 100.00 %
Elapsed time:                   4.75 secs
Data transferred:               3.76 MB
Response time:                  0.00 secs
Transaction rate:            2929.05 trans/sec
Throughput:                     0.79 MB/sec
Concurrency:                    3.11
Successful transactions:           0
Failed transactions:               0
Longest transaction:            0.03
Shortest transaction:           0.00
$ siege -b -c4 -t5S http://localhost/hello.php
** SIEGE 2.68
** Preparing 4 concurrent users for battle.
The server is now under siege...
Lifting the server siege..      done.
Transactions:                  13596 hits
Availability:                 100.00 %
Elapsed time:                   4.90 secs
Data transferred:               3.66 MB
Response time:                  0.00 secs
Transaction rate:            2774.69 trans/sec
Throughput:                     0.75 MB/sec
Concurrency:                    3.62
Successful transactions:           0
Failed transactions:               0
Longest transaction:            0.02
Shortest transaction:           0.00
$ siege -b -c4 -t5S http://localhost:6767/handlertest/hello 
** SIEGE 2.68
** Preparing 4 concurrent users for battle.
The server is now under siege...
Lifting the server siege..      done.
Transactions:                  10063 hits
Availability:                 100.00 %
Elapsed time:                   4.01 secs
Data transferred:               0.12 MB
Response time:                  0.04 secs
Transaction rate:            2509.48 trans/sec
Throughput:                     0.03 MB/sec
Concurrency:                    3.97
Successful transactions:       10063
Failed transactions:               0
Longest transaction:            0.15
Shortest transaction:           0.02

Le code est simple :

class Hello
{
    public function hello($request, $match)
    {
        return new \photon\http\Response('Hello World!', 'text/plain');
    }
}    

J’ai fait tourner plusieurs fois les tests de 5 secondes, au delà, ma pauvre machine n’arrivait pas à tenir la charge avec un épuisement des sockets. C’est bien entendu du test non optimisé, sur un machine de développement, donc bon, je n’ai aucune idée de ce que cela donnerait avec une machine de production. Pour comparer avec les autres frameworks, j’ai juste fait une règle de 3 sur les performances relative à la ligne de base de PHP. Oh, et cette comparaison c’est aussi une comparaison poire, pomme et orange, d’un côté vous avez Apache + mod_php, de l’autre Mongrel2 + Photon, juste le langage de programmation de l’application web est le même, tout le reste est différent, Photon est déjà dans le futur, on ne rattrape pas un photon…

Si vous vous posez la question de pourquoi Photon n’est pas disponible en téléchargement dès maintenant, c’est que l’application Hello World! de base sera un système de chat temps réel capable de supporter plusieurs milliers d’utilisateurs et que la documentation sera vraiment bonne avec référence et tutoriels. Cela demande un peu de temps, mais un petit Hello World! ou un semblant de boutique en ligne seraient vraiment non révélateurs de l’intérêt d’un serveur d’application avec tâches asynchrones (Adieu Gearman) et synchrones (Memcached en 10 lignes de PHP) qui se déploie en production sur de multiples serveurs avec routage des requêtes automatique sans reconfiguration (comme Heroku!).

Mise à jour : Le chat en 30 lignes de code avec Photon.

Photon couvert à 100% par des tests

The 2011-02-01 at 08:31 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

100% test coverage for Photon

C’est avec grand plaisir que je peux annoncer que Photon a maintenant un taux de couverture en tests unitaires de 100%. Je me suis posé quelques questions pour savoir comment mettre en place les tests unitaires pour Photon, finalement j’ai décidé d’utiliser PHPUnit directement. Photon n’utilise donc pas un lanceur de tests particuliers, mais directement PHPUnit. L’intérêt est de pouvoir écrire des tests comme d’habitude et facilement pouvoir intégrer dans une application les tests de modules externes. PHPUnit est maintenant le standard en ce qui concerne les tests unitaires, c’est donc bon de pouvoir se baser sur le travail fait de longue date sur cette bibliothèque. Pour lancer les tests sans rapport HTML de la couverture du code, il suffit de taper $ photon selftest, avec le rapport en HTML, voici un example :

$ photon selftest --coverage-html=/tmp/photon
Photon 0.0.1 by Loïc d'Anterroches and contributors.
Using PHPUnit 3.5.5 by Sebastian Bergmann.

............................................................  60 / 117
.........................................................

Time: 3 seconds, Memory: 9.75Mb

OK (117 tests, 186 assertions)

Generating code coverage report, this may take a moment.
Code coverage report: /tmp/photon/index.html.

Pour le future de Photon, une règle de base du développement sera de toujours garder une couverture à 100% du code par des tests unitaires. Un bout de code n’aura pas le droit de séjour dans le dépôt sans les tests qui vont avec. Cela va permettre de passer à l’étape suivante, c’est à dire la mise en place d’un serveur d’intégration continue pour Photon. D’ailleurs voici ce qu’il reste à faire :

  • une application Hello World! avec un projet de base quand on tape $ photon init monprojet. Bien entendu, avec des tests unitaires complets ;
  • un site web pour photon-project.com ;
  • une documentation par fonctionnalité, par exemple, la gestion des sessions, de l’internationalisation, des accès aux bases de données ;
  • un livre blanc, en fait, une documentation sous forme de livre pour utiliser Photon. J’ai constaté en effet que beaucoup de personnes aiment un livre avec un exemple qui est développé au fur et à mesure des chapitres.

Vous pouvez télécharger Photon, mais n’oubliez pas que cela reste du code pour les développeurs doués et connaissant bien PHP.

Limitation des namespaces PHP

The 2011-01-28 at 19:36 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

Une petite limitation des namespaces PHP, vous ne pouvez pas utiliser une fonction d’un namespace dans un autre namespace ou l’espace global sans tirer avec vous au moins le dernier nom de l’espace.

Par example, supposez que vous ayez :

namespace Foo\bar\bong;
function operation($param)
{
    echo $param, PHP_EOL;
}

Maintenant, dans un autre namespace, vous ne pouvez pas faire :

use Foo\bar\bong\operation as mafonction;
mafonction('Bonjour !');

Vous pouvez utiliser le namespace bong et de là accéder à la fonction :

use Foo\bar\bong as b;
b\operation('Bonjour !);

Je voulais avoir les fonctions de traductions __ et _n dans un namespace et pouvoir faire l’import avec use des fonctions là où j’en avais besoin, ce n’est donc pas possible de faire cela, car cela voudrait dire se traîner un bout de namespace et visuellement cela serait vraiment désagréable.

Ouverture du code de Photon, framework PHP pour Mongrel2

The 2011-01-18 at 13:58 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

donc voilà, j’ai maintenant ouvert l’accès à tous au code de Photon, le framework PHP optimisé pour Mongrel2. Zed Shaw a fait un accueil bien sympa à ce framework avec le tweet :

Awesome, there’s a PHP framework built around Mongrel2 up for people to try: http://projects.ceondo.com/p/photon/ (not by me).

Photon va être ajouté à la liste des frameworks dédiés à Mongrel2.

Maintenant, qu’apporte Photon par rapport à un framework comme Pluf, Zend ou Symfony ? Voici une petite liste des points importants.

Photon est un serveur d’application léger

Photon tourne comme un process avec un unique thread. Il est très léger car un process consomme moins de 2Mo de mémoire quand il est en attente de servir une requête. Bien entendu, la mémoire en action dépend de votre code.

Photon est très rapide

Avec 3 processus Photon derrière Mongrel2, un simple "Hello World!" sort plus de 800 requêtes par seconde sur une petite machine (AMD dual core 4850e). Notez que le test inclut le routage complet de la requête jusqu’au code de la vue puis le renvoi.

Sur ma machine, avec tout en local en utilisant siege avec la commande: siege -b -c4 -t20S URL, j’obtiens:

  • Photon: 840.27 trans/sec
  • Baseline PHP: 1212.32 trans/sec
  • Pluf: 281.28 trans/sec

D’après les benchmarks de Symfony2, assurément bien optimisés pour Symfony2, on obtient que le framework est à 33% du baseline PHP. Photon arrive à atteindre 70% de la baseline PHP. Notez que c’est du benchmark de coin de table, sur une machine de dev, mais cela donne une bonne idée de la performance du trio Mongrel2, zeromq et Photon.

Voici les chiffres qui ne veulent rien dire :

Hello World! minimaliste pour Photon

Transactions:		       16503 hits
Availability:		      100.00 %
Elapsed time:		       19.64 secs
Data transferred:	        0.20 MB
Response time:		        0.00 secs
Transaction rate:	      840.27 trans/sec
Throughput:		        0.01 MB/sec
Concurrency:		        3.96
Successful transactions:       16503
Failed transactions:	           0
Longest transaction:	        0.03
Shortest transaction:	        0.00

Baseline PHP <?php echo "Hello World!"; ?> et Apache

Transactions:		       23228 hits
Availability:		      100.00 %
Elapsed time:		       19.16 secs
Data transferred:	        0.71 MB
Response time:		        0.00 secs
Transaction rate:	     1212.32 trans/sec
Throughput:		        0.04 MB/sec
Concurrency:		        3.58
Successful transactions:       23229
Failed transactions:	           0
Longest transaction:	        0.04
Shortest transaction:	        0.00

Hello World! Minimaliste pour Pluf et Apache

Transactions:		        5378 hits
Availability:		      100.00 %
Elapsed time:		       19.12 secs
Data transferred:	        0.16 MB
Response time:		        0.01 secs
Transaction rate:	      281.28 trans/sec
Throughput:		        0.01 MB/sec
Concurrency:		        3.97
Successful transactions:        5378
Failed transactions:	           0
Longest transaction:	        0.06
Shortest transaction:	        0.00

Photon scale sans configuration avec plus de process sur votre LAN

Vous avez besoin brutalement de plus de puissance ? Lancez à la volée de nouveau process Photon pour répondre à la demande :

$ photon server start

Vous pouvez même vous mettre sur une autre machine sur votre LAN et taper:

$ photon server start

le process Photon va automatiquement contacter Mongrel2 sur l’autre machine et récupérer du travail, ceci sans aucun changement de configuration. C’est automatique et transparent, merci zeromq.

Photon est sympa avec les admins

Vous pouvez contrôler vos processus Photon facilement depuis la ligne de commande via une paire de socket de communication zeromq.

$ photon init monproject
$ cd monproject
$ photon server start
$ curl http://localhost:6767/handlertest/foo
Hello World!
$ photon server list
Waiting for the answers...
Photon id                     Uptime        Served  Mem. (kB)  Peak mem. (kB)
-------------------------------------------------------------------------------
loa-desktop-29874-1295296119  0d00:00:40    1       1517       1928
-------------------------------------------------------------------------------
1 Photon servers running. Memory usage: 1517kB.
$ photon server start
$ photon server list
Waiting for the answers...
Photon id                     Uptime        Served  Mem. (kB)  Peak mem. (kB)
-------------------------------------------------------------------------------
loa-desktop-29874-1295296119  0d00:01:24    1       1517       1928
loa-desktop-29896-1295296185  0d00:00:18    0       1352       1362
-------------------------------------------------------------------------------
2 Photon servers running. Memory usage: 2870kB.
$ photon server stop
Waiting for the answers...
Photon id                     Answer
---------------------------------------
loa-desktop-29896-1295296185  KO
loa-desktop-29874-1295296119  KO
---------------------------------------
2 Photon servers stopped.

Photon est conçu pour l’asynchrone

Il peut recevoir une requête et ne pas renvoyer de réponse pour laisser la réponse se faire envoyer par une autre process. Par exemple, votre client chat se connecte sur votre serveur, vous notez la connection et laissez un streamer envoyer les mises à jour. Le streamer peut être en PHP ou n’importe quel autre langage.

Bientôt, vous pourrez même dans le code de votre vue faire :

$task1 = new Task1();
$task2 = new Task2();
$manager = new TaskManager();
$manager->add($task1);
$manager->add($task2);
list($ans1, $ans2) = $manager->run(50);

les tasks seront exécutées en parallèle par des processus indépendants via zeromq et vous aurez l’assurance de ne pas bloquer plus de 50 ms. Après ce temps d’attente, les réponses non reçues ne seront pas attendues plus longtemps, vous pourrez alors retourner une réponse dégradée au client, mais une réponse rapide.

Vous avez lu jusqu’ici ? alors regardez le code et venez discuter sur la liste : photon.users@librelist.com. Il reste encore beaucoup de travail de nettoyage du code et de port des fonctionnalités de Pluf. Ce n’est pas que du copier/coller car le but est aussi de profiter au maximum des fonctionnalités de PHP 5.3 pour avoir un code simple, élégant et performant.

Pluf2 et vitesse de ma boucle de dispatch

The 2010-10-28 at 11:29 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

Je suis content de ma nouvelle boucle de dispatch pour Pluf2:

Transactions:		        6915 hits
Availability:		      100.00 %
Elapsed time:		        4.59 secs
Data transferred:	        0.08 MB
Response time:		        0.00 secs
Transaction rate:	     1506.54 trans/sec
Throughput:		        0.02 MB/sec
Concurrency:		        2.93
Successful transactions:        6915
Failed transactions:	           0
Longest transaction:	        0.02
Shortest transaction:	        0.00
Transactions:		       19503 hits 
Availability:		      100.00 %
Elapsed time:		       17.54 secs
Data transferred:	        0.22 MB
Response time:		        0.00 secs
Transaction rate:	     1111.92 trans/sec
Throughput:		        0.01 MB/sec
Concurrency:		        1.93
Successful transactions:       19503
Failed transactions:	           0
Longest transaction:	        0.02
Shortest transaction:	        0.00

C’est pour sortir "Hello World!". Un tout petit message de 12 octets (hors en-têtes), ceci explique le débit en Mo/sec ridicule. Quand je tape contre un petit fichier statique de 13 octets avec 2 ou 3 connexions simultanées, je récupère sur ma machine 2400 à 3000 req/s. En gros, Pluf 2 n’est que 2 fois moins rapide pour afficher "Hello World!" que pour une page statique !.

Il reste quelques bémols :

  • les middleware de Pluf ne sont pas encore gérées ;
  • et la gestion des exceptions dans une vue.

Mais bon, cela ne devrait pas trop affecter les performances. Du moins, je l’espère, je n’ai plus trop en tête le coût du rajout d’un bloc try {} catch () {} en PHP.

Pour ceux qui trouvent qu’un test "Hello World!" n’est pas réaliste, oui, ce n’est pas une réponse normale, c’est fait pour tester l’impact du framework par rapport à du PHP tout simple ou du fichier statique de même taille. Cela cherche à répondre à la question : Quel est le coût du framework sur une requête la plus simple possible ? Si pour votre application, vous devez renvoyer une page en 50 ms au visiteur et que juste le fait de charger la machinerie de votre framework pour un hello world vous coûte 45 ms, il ne vous reste que 5 ms de temps pour faire le calcul et rendu de la page. Si votre framework fait la même chose en 25 ms, cela vous donne 25 ms de temps, soit 5 fois plus de temps !.

Habitudes pour la montée en charge des applications web PHP

The 2010-06-07 at 18:51 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

ou pourquoi je hais les présentations faites pour des experts qui ensuite collent leurs slides sur le réseau. J’ai déjà critiqué pour cela un des membres actifs de Symfony et je vais vous faire une lecture critique des slides de Habits of Highly Scalable Web Applications.

Notez que probablement, dans le podcast de 1h, des explications plus détaillées sont distillées mais personne ne va l’écouter et tout le monde va regarder le pdf.

Le problème de base est que vous ne trouverez pas le moindre vrai point d’interrogation dans ces slides alors que la base pour la montée en charge c’est de se poser les bonnes questions et d’avoir les mesures pour prendre ensuite les bonnes décisions.

En gros, il présente du déjà dit et donne comme chemin pour la montée en charge de l’application web :

  1. on commence tout simple avec un serveur
  2. puis on découpe serveur web + base de données
  3. puis on ajoute des machines sur chaque niveau en mettant des esclaves pour la base de données
  4. puis on met en cache (c’est traité en dernier dans les slides, mais bon, on sait qu’il faut faire cela avant les esclaves)

Comme les questions sont rhétoriques sans fondamentaux, les réponses sont directes sans nuance et analyse :

  • slides 13/14 l’option de mettre un slave en face de chaque serveur est rejetée sans faire remarquer que si le load balancer sur le web serveur hash par ip, on peut faire travailler chaque slave pour un groupe d’utilisateur donné et donc avoir une meilleure mise en cache des données au niveau de l’OS et donc éviter des accès disques inutiles tout en gardant la flexibilité de passer de l’un à l’autre en cas de besoin si on a une erreur.
  • l’étape 4 du partitioning arrive après les slaves. Pourquoi ? Google partitionne avant de faire du slave car l’index ne tient pas sur un serveur. Cheméo c’est pareil, je dois faire du partitionnement avant d’avoir des slaves car sinon je finis par faire de l’accès disque.
  • slide 31 pour les bonnes pratiques de mise en cache, write through cache arrive en premier. Il oublie que cela dépend des besoins, on peut avoir une cache qui dit, je n’ai rien et donne rien (perte "partielle" de l’information) et on laisse en tâche de fond un write back qui va mettre à jour.

En gros, une liste de recettes mais pas le pourquoi. Le truc pour faire grandir votre site est simple, tout simple, tellement que cela ne permet pas d’écrire un livre et vendre des heures de consultant par milliers. Il est connu depuis des dizaines d’années dans l’industrie, c’est connu dans le grand public sous le nom de la méthode de Toyota et cela s’appelle la gestion intégrale de la productivité ou total productivity management (TPM).

TPM, c’est simple :

  1. on mesure tout ce qui se passe ;
  2. on cherche les goulots d’étranglement ;
  3. on supprime les goulots en utilisant les bons moyens ;
  4. on recommence ad vitam eternam.

Entre la productivité d’une raffinerie pour mieux convertir le pétrole en essence et diminuer les besoins d’aller faire de l’ultra deep offshore et la productivité de votre site web (la capacité à répondre aux requêtes dans les temps impartis) il n’y a pas de différence de méthodologie.

Mais cela veut dire que la première étape pour un site qui monte en charge c’est de tout écrire et garder des logs de performance pour savoir exactement quels sont les points problématiques. C’est ensuite que l’analyse des tendances est possible et qu’il est alors possible d’agir avant que le problème ne survienne et que si le problème survient, on peut faire quelque chose en connaissance de cause.

La prochaine fois que vous lisez un document avec des recettes, posez-vous la question du pourquoi avant d’appliquer une recette et vérifier d’où vient votre problème avec vos logs. Toutes les recettes dans cette présentation sont bonnes, mais il manque le contexte, voilà pourquoi je hais le powerpoint sur le réseau.

Quel framework PHP/Python/Ruby choisir si vous devez apprendre un nouveau aujourd'hui ?

The 2010-04-27 at 09:33 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

Supposez que vous codez avec Symfony et que vous le trouvez lourd et n’avez pas la patience d’attendre la version 2 qui devrait améliorer cela ou que vous codez avec Pluf et que si vous le trouvez super rapide vous voulez découvrir autre chose et vous êtes séduit par les sirènes de Django ou Ruby on Rails. En gros, vous voulez changer et vous ne savez pas très bien quel framework choisir pour votre changement. Si vous avez la chance de pouvoir prendre le temps d’apprendre comme vous voulez, voici mon avis, issu de 15 ans de développement de sites internet :

  • Symfony : Poubelle.
  • Pluf : Poubelle.
  • RoR : Poubelle.
  • Django : Poubelle.
  • Votre framework inspiré de Django/RoR/etc… : Poubelle.

Une autre question ? Poubelle.

Si vous devez apprendre quelque chose aujourd’hui pour demain, n’investissez pas votre temps dans des frameworks conçus sur des concepts vieux de 10 ans. Investissez votre temps dans des concepts nettement plus prometteurs.

Les problèmes des frameworks actuels

Le premier problème est l’ORM. Vous allez traîner avec plus ou moins de succès une correspondance entre votre base de données SQL et votre code orienté objet. Par exemple pour la homepage de The Onion, codé avec Django, ils ont besoin de 800 ms pour générer le SQL et 400 ms pour faire les requêtes et cela ne prend même pas en compte le coût pour convertir le résultat de la requête en objets Python.

Que vous utilisiez un framework ou un autre, vous aurez ce problème dans la majorité des cas.

Le second problème vient du langage et de la manière d’écrire votre code car il est toujours implicitement séquentiel : Faire A, puis B, sinon C, puis D, etc… Je prenais l’exemple suivant avec l’excellent piouPiouM (Si vous cherchez un excellent développeur web sur la région lyonnaise, il est bientôt dispo) :

Avec Indefero vous listez le contenu d’un répertoire de votre dépôt Subversion, le répertoire contient 5 fichiers à 5 révisions différentes. Combien vous faut-il d’appels systèmes à Subversion pour les lister avec le message de commit de chaque révision ? 6 appels. 1 pour la liste, 5 pour les 5 révisions. Supposez qu’il faille 15 ms par appel pour récupérer l’information, cela donne un temps de 80 ms pour récupérer l’information. Pourtant, vous pouvez faire cela en 40 ms.

Vous récupérez la liste, puis vous lancez en parallèle les 5 appels pour récupérer les messages de commit. Comme faites vous cela en PHP/Ruby/Python ? Vous ne le faites pas, car les bibliothèques sont codées pour bloquer sur les appels systèmes et le langage lui-même est conçu pour ne pas vous permettre de faire cela. (Ok, vous avez Twisted, mais ce n’est presque plus du Python…).

Votre langage et votre framework sont lourds via l’ORM et par définition non performants de part leur structure.

Le premier sauveur, node.js

node.js est un projet qui repart d’une feuille blanche et est conçu pour ne jamais bloquer. Vous programmez avec des callbacks et toutes vos actions peuvent être lancées simultanément. Surtout, toutes les bibliothèques sont conçues pour ne pas bloquer. Vous pouvez donc exécuter en parallèle des séries d’actions pour récupérer les informations qui vont être utilisées pour construire votre page. Vous laissez le soin à votre OS de gérer la pile des accès disques pour avoir une exécution dans le meilleur sens…

Le second sauveur, JSON

Si vous n’avez pas besoin des fonctionnalités ACID de votre RDBMS, utilisez autre chose (même un simple stockage dans des fichiers textes) mais stockez au format JSON. Vous pourrez stocker aussi cela dans MongoDB. JSON, c’est simplement des listes, dictionnaires, en gros, un stockage simple des structures de données que vous avez normalement dans votre programme, pas de lourdeur dans la conversion entre votre stockage et votre code.

Au final, node.js, JSON et MongoDB

Si je devais démarrer un projet avec un peu de temps devant moi je prendrais :

  • Node.js pour le faire tourner.
  • MongoDB pour stocker les données.
  • JsonTemplate pour formatter les données.
  • Un système à la Pluf pour router les requêtes.

Cela donne un système haute performance, non bloquant, sans la lourdeur d’un ORM et nettement plus "future proof" que le gros des troupes du moment.

Bien entendu, je continuerais d’utiliser Indefero pour héberger mon code (non, ceci n’est pas une publicité).

Funnel analysis avec Pluf, PHP et MongoDB

The 2010-04-10 at 16:09 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

Si vous n’avez pas lu l’article sur les tests A/B avec Pluf, c’est le moment de le lire avant de revenir ici.

Quand on cherche à vendre ou à faire faire une action par une personne sur un site, cela correspond souvent à de multiples étapes successives. Il est alors intéressant de suivre les pertes le long des étapes, c’est l’analyse de l’entonnoir ou funnel analysis. Le but est de savoir quelle étape doit être améliorée. Pluf permet de faire cette analyse en une ligne de code par étape. Le résultat est le suivant :

Funnel analysis with Pluf

Vous pouvez voir pour le funnel les pertes à chaque étape et le résultat final de 11,30%, vous pouvez aussi voir pour les propriétés de vos visiteurs (vous les configurez comme vous voulez) la conversion par étape et au final. Ici je filtre sur la propriété "month_price" et je peux constater que si elle vaut 1 j’ai un taux au total de 9.9% et si elle vaut 0 j’ai un taux de 12.18%. Et oui, "month_price" est en fait un test A/B. Je peux suivre l’impact de mon test A/B tout au long de mon funnel !

Et dans le code ?

Fidèle à l’esprit et la forme de Pluf, le code PHP est simple et élégant, totalement inspiré de MixPanel.

Pour noter une étape dans le funnel :

Pluf_AB::trackFunnel('forge_creation', 1, 'Plans', $request);

C’est l’étape 1, nommée Plans du funnel forge_creation.

Pour ajouter des propriétés de vos visiteurs à suivre :

 Pluf_AB::register($request, array('month_price' => $month_price));

Et là, $month_price peut être la valeur d’un test A/B, le pays du visiteur, etc. Vous pouvez en ajouter autant que vous voulez et vous pouvez les ajouter à tout moment dans vos vues.

Oui, c’est tout, rien d’autre, nada, une ligne et boum cela fonctionne.

Tests A/B avec PHP, Pluf et MongoDB en 4 lignes de code

The 2010-04-08 at 19:55 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

Si vous voulez optimiser votre site pour augmenter votre taux de conversion, rendre votre site plus agréable pour vos visiteurs, une solution simple et efficace est de réaliser des tests dits "A/B". Un test "A/B" ou "split test" est tout simple:

Vous donnez à 50% de vos visiteurs la version A de votre site et aux autres la version B. Vous regardez ensuite quelle version marche le mieux.

A/B testing avec Pluf en PHP

Vous aimez jouer au poker ou aux dés ? Oui ? C’est très bien, car cela veut dire que vous avez une petite idée des statistiques. Un test A/B peut être très facilement analysé avec une série de tests statistiques standards, le test Z, pour savoir si c’est la chance qui vous annonce que la version A est meilleure que la B ou si c’est vraiment le cas. Cela vous permet aussi de savoir si vos dés sont pipés :).

À partir de ce test, on peut calculer la certitude avec laquelle on sait que la version A est meilleure que la B. Une certitude de 50% veut dire qu’on a aucune idée, c’est 50/50. Pour être vraiment certain, il faut avoir une certitude de 95% (ce n’est de la chance qu’une fois sur 20). Sur la capture d’écran, vous avez une certitude de 93%, c’est très bien, mais il vaut mieux attendre au moins une exposition à 100 ou 150 personnes par alternative pour considérer le test comme terminé.

Maintenant, si vous n’aimez pas les stats, mais que vous avez compris que cela va vous permettre d’améliorer massivement votre application en testant différentes pages, vous avez raisons. Si vous codez en PHP et que vous connaissez le framework Pluf, vous avez de la chance, le système de tests A/B est inclut en standard.

La première chose à faire est de lancer MongoDB pour stocker les résultats des tests et d’avoir Memcached ou APC comme cache Pluf histoire que les performances soient optimales.

Ensuite, vous mettez dans le code de votre vue:

$download_style = Pluf_AB::test('download_style', $request,
              array('normal', 'bold', 'italic', 'bolditalic'));

Cela veut dire que vous faites le test ‘download_style’ et que vous avez 4 alternatives. J’utilise la valeur de $download_style dans mon gabarit pour afficher le bon style.

Dans ce cas test, j’ai fait une vue qui fait la redirection vers la page de download du logiciel et dans cette vue je mets simplement avant la redirection :

Pluf_AB::convert('download_style', $request);

Cela veut dire que j’ai eu une conversion pour cette utilisateur. L’utilisateur est suivi par des cookies et le système est correctement résistant aux robots.

2 lignes de code pour un test A/B avec Pluf !

Maintenant pour voir les stats comme dans la capture d’écran, il vous suffit d’ajouter à la définition de vos urls, la vue suivante :

array('regex' => '#^/urlquevousvoulez/$#',
      'base' => $base,
      'model' => 'Pluf_AB_Views',
      'method' => 'dashboard',
      'name' => 'pluf_ab_dashboard'
      ),

C’est simple, efficace et comme d’habitude haute performance…

Utiliser assert pour faire du log à haute performance en PHP

The 2009-10-31 at 09:30 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

Imaginez que vous vouliez tracer des informations dans l’exécution de votre application web, en gros, logger des informations, vous trouverez très facilement des class PHP vous permettant d’ajouter cela dans votre code :

// mon code qui fait quelque chose
$logger->log('Mon information');
Mon_Logger::debug($variable);
// la suite du code etc...

Dans le premier cas vous loggez via une méthode d’un objet, dans le suivant via une méthode statique, cela pourrait aussi être via un code du genre Factory::get('logger')->log('message').

Cette approche a malheureusement 2 problèmes :

  1. une perte de performance du code en production quand on ne veut pas logger ;
  2. le manque de contexte au niveau du log, il faut souvent alors ajouter des informations dans le message pour savoir d’où vient le message.

La perte de performance peut être partiellement levée par l’utilisation de l’injection de dépendances et charger ainsi un objet de log stupide, qui ne fera rien à son appel. Mais on garde alors un triple coût au niveau des performances :

  1. coût de l’injection de dépendance pour charger le bon logger ;
  2. coût de la génération du message pour ensuite ne rien en faire ;
  3. coût de l’appel à une méthode statique PHP utilisateur, qui ne fait rien.

Dans Pluf, je n’ai pas proposé de méthode particulière et je laisse les gens utiliser le système de leur choix car je n’avais pas trouvé de méthode qui limite vraiment la perte de performance en production, jusqu’à hier.

PHP dispose des assertions, les assertions permettent d’évaluer un morceau de code et d’agir si le résultat est false. Je vous laisse lire la documentation, c’est rapide.

Les côtés très intéressants des assertions sont :

  1. on peut les activer et les désactiver dynamiquement ;
  2. si on passe une chaîne en entrée elle est évaluée comme du code dans le contexte local ;
  3. on peut récupérer le nom du fichier et la ligne où l’exception a été exécutée via un callback.

Conclusion, vous pouvez mettre votre code à logger dans votre chaîne pour votre exception et si votre logger retourne false votre callback va se faire appeler. Si dans votre logger vous ne faites que stocker temporairement votre message, vous pouvez dans le callback reconstituer l’ensemble des informations et vraiment stocker dans votre fichier de log ou ailleurs.

Voici un bout de code pour illustrer :

<?php
function log_assert($file, $line, $code)
{
    $GLOBALS['log'][] = array(
        microtime(true),
        $file, $line, $code,
        isset($GLOBALS['last_assert_res']) ? $GLOBALS['last_assert_res'] : ''
                              );
    $GLOBALS['last_assert_res'] = null;
}

function logger($text)
{
    $GLOBALS['last_assert_res'] = $text;
    // false va forcer l'appel à log_assert
    return false; 
}

// Active les assert en les rendant silencieuses
assert_options(ASSERT_ACTIVE, 1); 
assert_options(ASSERT_WARNING, 0);
assert_options(ASSERT_QUIET_EVAL, 1);
assert_options(ASSERT_CALLBACK, 'log_assert');

$assert = array(1, 0);
$n = 10000;
foreach ($assert as $active) {
    $GLOBALS['log'] = array();
    assert_options(ASSERT_ACTIVE, $active); 
    $start = microtime(true);
    for ($i=0;$i<$n;$i++) {
        assert('logger($i.' is an integer')');
    }
    $time = microtime(true) - $start;
    print "Assert active: $active\n";
    print "Elapsed time: $time\n";
    print "Per call: ".($time/$n)."\n";
    print "Log size: ".count($GLOBALS['log'])."\n\n";
    if (count($GLOBALS['log'])) {
        var_dump($GLOBALS['log'][0]);
        print "\n\n";
    }
}

Cela vous donnera un résultat du genre :

$ php test.php 
Assert active: 1
Elapsed time: 0.403494119644
Per call: 4.03494119644E-5
Log size: 10000

array(5) {
  [0]=>
  float(1256912192.17)
  [1]=>
  string(36) "/home/loa/Projects/pluf/tmp/test.php"
  [2]=>
  int(32)
  [3]=>
  string(27) "logger($i.' is an integer')"
  [4]=>
  string(15) "0 is an integer"
}


Assert active: 0
Elapsed time: 0.03799700737
Per call: 3.799700737E-6
Log size: 0

En gros, votre logger est 10x plus rapide en production si il ne logge rien. Le assert('code') coûte autant que le coût d’un appel de la plus simple des fonctions PHP comme is_numeric ou autre, totalement négligeable en mode production. Si on compare avec l’appel à une méthode statique utilisateur qui ne fait rien, le code qui logge ne coûte que 30% de plus ! La fonction assert est donc très optimisée et on peut en profiter.

Et comme vous avez un framework bien fait, vous pouvez toujours faire la chose suivante :

  1. en fonction de la requête (cookie, url, etc) activer ou non le log ;
  2. accumuler les logs pendant la requête ;
  3. stocker le log dans memcache ou APC en fin de requête ;
  4. flusher les informations sur le disque ou dans votre base de données toutes les x requêtes/minutes.

Notez que c’est la seule manière d’avoir le contexte du message au meilleur coût (pas d’introspection). Un inconvénient quand même, votre message est l’évaluation d’une chaîne de caractères, donc cela se lit moins bien dans le code. J’ai quand même trouvé une méthode pour logger dans Pluf, chouette.

Le rappel de l’existence de assert m’a été fait par François dans les commentaires ici, merci !

Mise à jour: Ceci est une utilisation détournée de la fonction assert, cela peut poser des problèmes si vous combinez cette approche avec du code utilisant assert pour réellement faire des tests. Ce qui me rassure c’est que dans l’intégralité du code PHP que j’utilise, incluant de nombreuses bibliothèques issues de PEAR, je n’ai pas trouvé une seule utilisation de assert.

Combiner type hinting et interfaces en PHP pour sécuriser son code

The 2009-10-27 at 09:42 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

Si vous faites du type hinting, c’est qu’à priori vous faites très attention à la structure de vos projets PHP. Si vous avez un code du type :

function maFonction(MaClass $objet) {
    // corps de la fonction
    $objet->faitTruc();
    echo $objet->bidule;
}

vous n’acceptez que des objets de type MaClass en entrée. Supposons que maintenant, une personne veuille utiliser votre fonction avec un objet de la class AutreClass en sachant très bien que son objet fourni les méthodes et propriétés nécessaires à maFonction. Il ne peut pas car AutreClass n’est pas MaClass.

Une approche élégante si vous voulez donner de la flexibilité à vos utilisateurs finaux est de dire : "J’accepte tous les objets qui implémentent la bonne interface.".

 function maFonction(IMaClass $objet) {
    // corps de la fonction
    $objet->faitTruc();
    echo $objet->bidule;
}

Maintenant, il suffit que AutreClass implémente l’interface IMaClass pour satisfaire maFonction. Cela donne de la flexibilité tout en forçant un peu de contrôle car PHP au niveau du langage va forcer le respect de ce contrat entre maFonction et le paramètre. Si vous avez une utilisation parcimonieuse des interfaces dans vos logiques métier (histoire d’éviter d’avoir ensuite des class qui implémentent 5 interfaces ou plus), cela vous donne une certaine assurance, surtout si votre code est ensuite utilisé par d’autres entités dans votre société. Vous êtes certain que la personne a été obligée d’implémenter l’interface avant d’utiliser votre fonction.

Une autre approche est de demander de manière implicite l’interface sans jamais le déclarer réellement dans le code. C’est à dire que la fonction va accepter n’importe quoi :

function maFonction($objet) {
    // corps de la fonction
    $objet->faitTruc();
    echo $objet->bidule;
}

C’est alors au moment de l’exécution dans la fonction que PHP va retourner une erreur si l’interface implicite n’est pas respectée. Ceci peut être très mauvais dans certains cas. Si on suppose que maFonction fait une série d’opérations sur des fichiers et que l’objet fournit les chemins nécessaires :

function maFonction($objet) {
    grosse_copie($objet->source(), $objet->destination());
    nettoyage($objet->sourceExcludeBackup());
    echo $objet->bidule;
}

On va supposer que grosse_copie fait une grosse copie de fichiers de la source vers la destination et que nettoyage va ensuite nettoyer la source mais garder le backup. Dans ce cas là, si l’objet ne fournit pas la méthode sourceExcludeBackup, PHP va s’arrêter après la grosse copie et laisser votre système dans un mauvais état. En ayant utiliser le type hinting, PHP n’aurait même pas commencé l’exécution de maFonction, le système aurait été protégé.

Le type hinting et les interfaces ont un impact au niveau des performances et complexifie le code, donc ces outils doivent être utilisés avec soin, particulièrement dans le cœur d’exécution de votre programme mais ces outils apportent une sécurité indéniable dans certains cas particuliers, surtout dans la logique métier, là où le non respect d’une interface explicite ou implicite peut faire des dégâts.

Un endroit où je n’utiliserais pas le type hinting et les interfaces est par exemple la boucle de dispatch dans une application web. De toute façon si vos objets de requête et de réponses n’implémentent pas les bonnes méthodes, vous allez vite vous en rendre compte et les dégâts seront une requête plantée. L’erreur ne prendra pas longtemps pour être trouvée et corrigée, et dans tous les autres cas vous payerez le prix au niveau des performances, ce qui serait dommage.

Bien utilisée, la combinaison type hinting et interfaces peut donc se montrer être un outil très puissant de contrôle de la qualité.

Pluf, le framework PHP le plus rapide du monde (troll)

The 2009-10-23 at 09:01 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

Si vous ne connaissez pas Pluf, le framework PHP le plus rapide du monde, je vous invite à lire une petit brève ici et un benchmark de framework PHP là, vous pouvez aussi lire comment je tire à vue sur certains développements ici avec un benchmark de template PHP là.

Pluf, le framework PHP le plus rapide du monde, c’est aussi un joli troll dans les chaumières des développeurs PHP, il semble que cela commence même à troller dans les SSII sur le sujet. Aïe, pour un programmeur, voir son code être le sujet d’un troll, cela peut paraître ennuyeux, mais dans mon cas, cela me fait très plaisir. Je ne suis pas sado, loin de là, mais essayer de faire passer une idée qui va à l’encontre des pratiques du moment est toujours difficile.

Donc à toutes les personnes qui se moquent, je vous dis merci, et je vous dis surtout continuez ! Continuez de troller sur la rapidité du framework X par rapport au Y.

Maintenant pourquoi je tape sur les bibliothèques et frameworks PHP qui ne font que prendre du poids avec le temps ?

InDefero est une application web dite de forge logicielle. Vous pouvez au choix la télécharger (licence GPL) ou profiter d’un hébergement pour vous contre quelques Euros. L’hébergement InDefero c’est 1500 forges dont 1300 d’actives. Quand vous gérez cela, vous gérez un système qui au niveau performance doit supporter 50 à 100 fois plus de charge qu’une installation unique (la charge se répartie dans la journée avec les fuseaux horaires, donc ce n’est pas un facteur 1000).

Un facteur de 50 à 100, cela veut dire que très rapidement, si vous utilisez des bibliothèques qui font trop, votre composant web va s’essouffler, en gros votre serveur web ne va plus ternir la charge tout seul. Il faut être réaliste, dans la majorité des applications web, la logique métier est principalement de la requête sur la base de données. Donc on peut faire un calcul simple, de derrière d’une enveloppe et décomposer une requête web en 2 parties, logique métier et framework :

  • logique métier: 50ms par requête ;
  • logique framework: 50ms par requête.

On obtient donc une réponse en 100ms pour une requête, performance tout à fait honorable, on va l’appeler le cas optimisé.

Maintenant, je tape dès que je vais des facteurs 3 à 5 sur les performances des bibliothèques. Comme la logique métier est incompressible on obtient :

  • logique métier: 50 ms par requête (ne change pas) ;
  • logique framework: environ 150 ms par requête (facteur 3).

Total 200 ms, un facteur 2.

Cela veut dire quoi ? Cela veut dire que votre ensemble DB + serveur web va avoir besoin de 2 fois plus de cycles CPU pour servir la même charge. Cela veut dire que votre coût pour faire croître votre site va être multiplié par 2.

En fait c’est pas très juste, le premier goulot dans une architecture share nothing est toujours la base de données. Donc dès que le système ne tient plus, on déporte la base sur son serveur propre. Maintenant, on refait le calcul mais on prend en compte la distinction serveur de BD et serveur Web.

Version optimisée par requête :

  • logique métier: 50ms par requête (serveur BD) ;
  • logique framework: 50ms par requête (serveur Web).

Version lourde :

  • logique métier: 50 ms par requête (ne change pas, serveur BD) ;
  • logique framework: environ 150 ms par requête (facteur 3, serveur Web).

Maintenant, si on suppose que dans le cas optimisé un serveur web peut saturer un serveur de BD, on obtient pour assurer la même charge :

  • Version optimisée : 1 serveur web et 1 serveur de base de données.
  • Version lourde : 3 serveurs web et 1 serveur de base de données.

Facteur 2 au total, mais un facteur 3 sur le front end. Je vous laisse penser à tous les problèmes de répartition de la charge et de synchronisation du code que cela implique ainsi que les coûts associés. Bien entendu c’est du calcul de dos d’enveloppe donc à prendre comme un ordre de grandeur surtout l’approximation logique métier équivalente aux requêtes vers la base de données.

Pourquoi un différence si importante ? Car à tous les échelons, le choix à été fait de prendre la solution qui fait plus au cas où qui n’arrive dans 99% des cas jamais mais qui coûte 3 fois plus de cycles. L’agilité c’est introduire dans un système la complexité strictement nécessaire et suffisante en choisissant les bons outils et en refactorisant en continu. C’est très dur (j’ai vraiment du mal) mais cela vaut la peine !

Note pour les trolls : je pars en WE et ne vais avoir le temps de répondre que lundi.

La philosophie d'Erlang appliquée à PHP : Don't worry about errors, just let it fail

The 2009-10-12 at 11:08 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

Si vous ne connaissez pas Erlang prenez le temps de vous documenter un peu sur le sujet et revenez.

Une chose que j’aime beaucoup avec Erlang et la philosophie suivante :

"let some other process fix the error".

C’est une philosophie non conventionnelle dans le domaine de la programmation de systèmes complexes et pourtant elle fonctionne très très bien. Elle fonctionne très bien non pas parce que les programmeurs Erlang sont meilleurs que les autres, mais de part la nature du langage et de la VM associées et par l’utilisation abondante du principe :

Check at the interfaces and trust inside.

Grosso modo, faites votre travail de vérification en entrée mais ensuite faites confiance une fois les données dedans.

La rapidité de Pluf provient en très grande partie de l’application de ces choix, pas de type hiting, pas d’interfaces, le minimun de vérifications en interne et surtout un error_handler qui va très joliment retourner toutes les erreurs aux administrateurs avec une copie de la pile.

Pourquoi pas de type hinting et pas d’interfaces ?

Je réponds par une question : Pourquoi forcer le type et l’interface au niveau du code quand cela coûte des cycles d’exécution et que de toute façon vous faites des tests unitaires de vos programmes ? Quel bénéfice cela apporte-t’il vraiment ? Quel problème peut apporter le duck typing ?

La performance de PHP vient de sa simplicité, la résilience d’Erlang vient de la simplicité de son modèle de process… La performance vient du dépouillement pour aller à l’essentiel.

Saint Exupéry écrivait "Il semble que la perfection soit atteinte non quand il n’y a plus rien à ajouter, mais quand il n’y a plus rien à retrancher." et ce qui aurait très bien pu être la réponse d’Einstein : "Everything should be made as simple as possible, but not simpler."

Note : Je ne suis pas un programmeur de formation, mon travail qui me fait vivre est d’optimiser ça, j’ai donc une déformation professionnelle pour la simplicité et la performance et je prends donc des raccourcis dans le formalisme de la programmation. Chacun voit midi à sa porte, c’est bien connu.

Pluf Template 3x plus rapide et 2x moins gourmand que Twig

The 2009-10-12 at 06:58 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

La sortie de Twig annoncée par Fabien Potencier m’avait fait hurler. Twig est un système de gabarits pour PHP comme il en existe des centaines. En fait, au bout d’un certain temps, tout développeur s’est essayé à la création d’un système de gabarits/templates pour bien délimiter la logique métier de la présentation.

J’ai hurlé car Twig est une très grosse bibliothèque (presque 100 fichiers) et que cela m’a fait tout de suite penser à mes tests de performances de Pluf.

Je n’avais jamais fait la moindre optimisation du système de gabarits de Pluf et ma version n’est qu’une version simplifiée et étendue de jTpl, le système du framework Jelix, une bonne occasion de faire des tests.

J’ai d’abord mis en place des tests dont voici l’archive complète. Les premiers runs ne montraient pas de différence significative entre Twig et Pluf_Template. Frustration, comment Twig, pouvait être grosso modo aussi rapide que Pluf_Template ?

Un petit tour dans les entrailles de Twig via xdebug et je découvre que :

  1. effectivement Twig est très lourd ;
  2. mais que l’auteur est futé, le test est taillé sur mesure pour favoriser Twig.

Twig compile les gabarits en une ou plusieurs classes PHP, donc un rendu n’est qu’un appel à une fonction d’une classe. Pas besoin de faire un include comme pour Pluf. Twig génère des classes lourdes en réinventant PHP, d’où le besoin de ne faire que 3 itérations affichages dans ces tests car sinon les performances se dégradent trop. Un rendu ne devient dans ce test qu’un echo et 3 appels de fonctions.

J’ai donc repris cette idée d’une classe par gabarit et fais quelques petites optimisations par endroits, voici les résultats pour le gabarit Twig suivant :

base.html :

{% autoescape on %}<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    {% block head %}
      <link rel="stylesheet" href="main.css" />
    {% endblock %}
  </head>
  <body>
    {% block content %}{% endblock %}
  </body>
</html>
{% endautoescape %}

content.html :

{% extends "tbase.html" %}
{% block content %}
{% for item in items %} item {% endfor %}
{% endblock %}

Vraiment très simple, en fait un peu trop simple mais cela correspond aux tests faits par Fabien, donc cela permettra de faire une comparaison.

Les tests ont été faits sur 3 et 50 éléments dans items avec respectivement 10000 et 1000 rendus. Pour chaque test, 10 runs et les 9 meilleurs ont été conservés. Tous les temps sont sans le coût de la compilation.

Performances
SystèmeTestRendus/sComparaison
Twig3/1000013767 -
Pluf3/10000351512.55 fois plus rapide
Twig50/10001325 -
Pluf50/100043003.24 fois plus rapide

Twig est nettement plus lent, rien à commenter.

Mémoire
SystèmeTestPic mémoire avec compilation (ko)Pic mémoire sans (ko) Comparaison
Twig3 itérations/1 renbu996 342 -
Pluf3 itérations/1 rendu4902631.3 à 2 fois moins gourmand
Twig50/10001330 691 -
Pluf50/10004952652.6 fois moins gourmand

Les chiffres parlent d’eux-même, mais surtout, ce que je trouve très étonnant, c’est l’augmentation de l’utilisation mémoire de manière aussi importante de Twig en fonction du nombre de rendus. Des objets qui perdent de la mémoire ? Ce n’est pas très propre et je n’arrive pas à m’expliquer ces différences. Le temps de compilation est accessoire, Pluf_Template ne peut que être plus rapide mais on peut très bien précompiler tous les gabarits.

Maintenant, pourquoi battre Pluf_Template sera très dur pour quiconque ?

  • Compilation : Laurent Jouanneau a eu la meilleure idée qui soit, Pluf_Template utilise directement le tokenizer de PHP, c’est le code C le mieux optimisé de PHP.
  • Exécution : Le code PHP résultant est du code avec le minimum de constructions et d’appels à des fonctions. Le seul gros travail est l’autoescaping. Chose qui ne peut pas être évité dans la majorité des cas.
Conclusion

Vive le logiciel libre et la simplicité. La simplicité de Pluf_Template m’a permis d’améliorer ses performances en moins de 30 minutes grâce à ce que j’avais appris via le code de Twig. Gardez vos frameworks légers pour que la mascotte de PHP ne devienne pas un objet de moqueries…

Twig et template de Pluf, effet marketing d'un système de templates pour PHP

The 2009-10-12 at 10:08 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

Mise à jour : Un test "rigoureux" est disponible maintenant.

Fabien Potencier vient d’annnoncer Twig un système de gabarits pour PHP. En dehors du fait que le texte d’annonce commence par quelque chose qui ressemble à du copier/coller de la documentation du framework PHP Pluf, ce qui m’honore, le projet annonce des fonctionnalités exclusives qui en fait ne le sont pas.

Cela fait plus de 2 ans que Pluf propose l’auto échappement des variables dans les gabarits et cela même avant Django. Regardez le système de gabarit de Jelix dont est issu celui de Pluf, tout est dedans (sauf peut-être l’héritage mais il est dans celui de Pluf). Vous constaterez d’ailleurs que Fabien se garde bien de mentionner ces systèmes dans son article… manifestement il connaît bien les effets marketing des omissions bien choisies comme nos amis de Microsoft, Apple, etc…

Et c’est stupide, carTwig apporte une valeur ajoutée certaine, le mode "bac à sable" qui permet l’exécution dans un contexte restreint. C’est là la réelle valeur ajoutée de ce système de gabarit car tout le reste existe déjà, alors pourquoi ne pas mettre l’accent dessus ?

Mise à jour: Twig c’est une bibliothèque de presque 100 fichiers et 160ko, le système de templates de Pluf c’est 10 fichiers dans 70ko en comptant les tags optionnels et les exemples. J’ai un nouveau slogan pour Sensio, "Nos produits sont conçus pour vendre du service associés et des clusters pour votre application web!". Les DSI doivent se frotter les mains, cela leur fait plus de chiffre à gérer. Mais bon, je préfère l’agilité et la légèreté, chacun son truc.

Mise à jour le 10 octobre: Twig est très rapide, mais après un petit coup de xdebug (les 15 premières minutes que je passe pour optimiser le code du système de Pluf) je passe de 5% plus lent à 5% plus rapide que Twig.

Mise à jour 10 minutes après: Le benchmark de Fabien n’est pas correct on va dire. Twig est 4 fois plus lent que Pluf_Template pour 1000 rendu d’une itération sur 1000 éléments ! En gros, c’est un benchmark calculé pour le cas idéal de Twig. Par contre il y a une bonne idée dans Twig que je vais inclure dans Pluf_Template. Au lieu de faire un include à chaque rendu, le code PHP est en fait une classe avec une méthode de rendu. Donc pas besoin de 1000 include. En pratique, dans une application web, cela ne change pas grand chose, car généralement on ne fait le rendu que de 2 ou 3 templates au maximum par requête. Je vais creuser cela même si Pluf_Template est déjà plus rapide et sera toujours plus rapide que Twig. Pourquoi ? Car il fait nettement moins pour aboutir à la même chose. La meilleure façon d’aller vite est de reste simple et ne rien faire d’inutile.

Mise à jour finale ? En implémentant l’idée de la class par template, Pluf_Template est 3 à 4 fois plus rapide que Twig dans tous les scénarii. Je mets en ligne le code demain soir, car là c’est du code rapide entre les petits fours d’un mariage.

Surflexibilité d'un framework agile (PHP, Python, Ruby ou autre)

The 2009-07-04 at 13:29 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

Un très bon article qui illustre bien pourquoi je hurle quand je vois des frameworks introduire toujours plus de flexibilité pour des raisons très souvent d’idéal d’implémentation technique au détriment des performances et de la facilité de compréhension du code : Premature Flexibilization Is The Root of Whatever Evil Is Left.

Je n’ajoute de la flexibilité dans Pluf que si j’ai réellement besoin et si je fais cela, je m’assure de suivre la règle du projet WebKit : "The way to make a program faster is to never let it get slower".

Auto admin avec Pluf, tout simplement

The 2009-05-14 at 13:28 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

Je suis en train de passer le site de Céondo sous Pluf. Le but est de pouvoir facilement ajouter un carnet et de rendre ce site plus dynamique au niveau de son contenu. J’ai décidé que toute son administration passera via l’auto admin de Pluf. Voici donc une capture d’écran de l’ajout d’une ressource.

Ajout d'un objet avec l'auto admin de Pluf

Pour avoir vos modèles dans l’auto-admin, vous devez simplement créer un fichier papp.php dans le répertoire de votre application avec par exemple le contenu suivant :

<?php
return array('path' => 'ceo',
             'name' => 'Ceondo',
             'models' => array('page' => 
                               array('model' => 'CEO_Page',
                                     'list_display' => 
                                     array('title', 
                                           'path',
                                           'category', 
                                     ),
                                     )
                               )
             );

Cela veut dire que l’application Ceondo va avoir le modèle CEO_Page et que pour l’affichage dans la liste je veux les trois champs title, path et category. Vraiment tout simple… car le formulaire va directement être créé à partir de la définition du modèle.

Auto admin avec Pluf - Redux

The 2009-05-13 at 12:36 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

Juste une petite note pour vous annoncer que l’auto-admin de Pluf arrive enfin… Le style est encore à revoir, il reste encore beaucoup de choses à faire au niveau de la gestion des permissions et du suivi des changements, mais la base est là. Je pousse le code dans le dépôt cette semaine. Cela ne sera pas encore parfait, mais cela sera une bonne base qui intéressera je pense déjà beaucoup de monde.

Auto admin à la Django avec Pluf

Mise à jour : Le code est disponible.

Yummy, version 3.1.1 d'APC

The 2009-02-19 at 13:10 by Loïc d'Anterroches filed under Pluf - Framework en PHP5.

APC, Alternative PHP Cache, introduit avec la version 3.1.1 les fonctions apc_inc, apc_dec et apc_cas.

Voici comme les utiliser dans vos scripts :

<?php
apc_add('count', 0); // Cela fait la création uniquement si 'count' n'éxiste pas
$i = apc_inc('count', 7); // incrémente de 5 la valeur de count.
$i = apc_dec('count', 3); // décrémente de 3 la valeur de count.
apc_cas('count', 4, 10); // si 'count' == 4, alors mettre 10 dans count
echo $i; 
?>

Ces opérations sont dites comme atomiques, cela vous permet donc de faire un compteur efficace car vous gardez en mémoire le compteur et non avec un accès disque nécessitant un système de verrou pour les accès en lecture/écriture. Vous pouvez ainsi régulièrement faire la copie du nouveau compteur sur le disque ou votre base de données et utiliser dans la majorité des cas le compteur d’APC.

Next Page


Logo of Plume CMS