Le javascript en multithread? Oui, avec les Web Workers!

 

web_workers3

Historiquement, le javascript est un langage s’exécutant dans un environnement mono-thread (c’est-à-dire que tous les événements s’effectuent les uns après les autres). Lorsque une fonction longue était réalisée, l’interface graphique ne répondait plus, tout était figé. Cela est particulièrement gênant dans le cas où des animations ont lieu, ou si une vidéo est en cours de lecture.

Je vais vous présenter dans ce billet la solution qui a été trouvée pour répondre à ce problème : l’introduction des normes HTML5 et plus particulièrement les « Web Workers ».

Optimisation Magento – Mise à plat des tables Customer

magento_logo

Un des atouts de la plateforme E-commerce Magento est son architecture de base de données dite « EAV ». Cette architecture offre une grande flexibilité lorsqu’il s’agit par exemple d’ajouter un attribut sur l’entité Client ou Produit. L’avantage vient du fait que chaque ajout d’attribut ne va modifier en rien la structure des tables car la liste des attributs ainsi que leurs valeurs sont stockées dans des tables bien distinctes.

Audit PHP : sécurité et bonnes pratiques

PHP_LOGO

Créé en 1994, PHP (Hypertext Preprocessor) est un langage de programmation très répandu sur Internet.

C’est un langage puissant, facile à aborder et permissif.
De ce fait, il est fréquent de retrouver des codes PHP qui ne respectent pas les normes élémentaires de sécurité et de bonnes pratiques.

Cet article est un aide-mémoire qui dresse une liste (non exhaustive) des points à contrôler dans le cadre d’une revue de code.

Récupération du « Load average » avec JMeter

Introduction

Je ne ferais pas ici une présentation complète de JMeter et JMeter Plugins, qui permet entre autres de récupérer les métriques serveurs pendant l’exécution des tests comme la charge CPU, la mémoire utilisée… Mais ce plugin ne permet pas de récupérer une métrique plus intéressante que la charge du CPU du serveur, le « Load average ».

C’est la récupération de cette métrique qui nous intéressera dans cet article.

Le « Load Average »

Le « load average » désigne, sous les systèmes UNIX, une moyenne de la charge système sur une certaine période. C’est donc une mesure de la quantité de travail que fait le système durant la période considérée. Conventionnellement les périodes utilisées sont 1, 5 et 15 minutes. Cette mesure est disponible via la commande « top » ou « uptime », ou encore via le fichier système « /proc/loadavg ».

Performance statistics : Perf4J pour le code java

Le framework Perf4J offre une façon élégante de faire des statistiques de performance de notre code java.

Le site Perf4J donne cette analogie qui résume bien son apport :

 »Perf4J is a set of utilities for calculating and displaying performance statistics for Java code.
For developers who are familiar with logging frameworks such as log4j or logback, an analogy helps to describe Perf4J:

Perf4J is to System.currentTimeMillis() as log4j is to System.out.println() »

Ainsi comme le log4J (ou toute autre implémentation de trace log) est devenu un standard (norme), le Perf4J pourrait aussi l’être.

Notez que le terme de performance est employé dans un sens très large. Seul le temps de réponse nous préoccupe.

La démo présentée ci-après est construite avec Perf4J basé sur l’AOP de Spring ce qui permet de ne pas polluer le code existant.

Pour réaliser cette démo de façon non intrusive, l’annotation @Profiled de Perf4J et l’AOP de Spring sont utilisés.

Les versions de Spring 2+ et jdk5+ sont utilisées.

Passons à la mise en pratique de ce framework avec l’AOP Spring.

Soirée JVM Performance – Paris JUG

Lors de la soirée « JVM Performance » organisée le 11/09/2012 par le ParisJUG, William Louth (@williamlouth sur twitter et william (at) inspired.com par email) de la société jinspired nous a sensibilisé à la problématique d’exploitation de la JVM.

Constat

Il part du constat, qu’en général, lorsqu’une alerte est détectée sur une JVM (alerte rouge nagios par exemple), la réaction des exploitants est :

  • Attendre que cela respasse au vert !
  • Faire un kill -9 de la jvm

Les exploitants ont finalement peu d’outil et ne peuvent pas vraiment analyser ce qui se passe à un instant T sur la JVM. La problématique est encore plus grande lorsqu’on essaye de connaître la qualité de service ressentie par les utilisateurs alors qu’on est en mode distribué dans le « cloud ».

Les logiciels que nous développons ne prennent pas en compte l’environnement dans lequel ils tournent (puissance de chaque nœud par exemple). Les traitements ne s’adaptent pas à l’environnement mais essayent de délivrer un résultat le plus rapidement possible.

Par exemple, lorsqu’on doit persister une information dans une base de données, le goulot d’étranglement étant cette base, il est probable qu’à sollicitation importante, certains Threads commencent à se bloquer. Or, un Thread qui se bloque, c’est de la mémoire de la JVM qui reste utilisée et on imagine très bien quelle sera la conséquence pour le Garbage Collector (GC) qui va commencer à passer de plus en plus souvent pour libérer de la mémoire.  Le GC va consommer de plus en plus de ressource processeur et au final, les Threads se bloqueront définitivement (spirale infernale).

L’idée défendue par William peut consister par exemple à ralentir certains traitements lorsqu’on constate que les accès à la base commencent à ralentir. Il s’agit d’implémenter une auto régulation par le logiciel lui même (Software Regulation Control).

La collecte

Pour cela, il est nécessaire de collecter de l’information sur les différents traitements et de les observer dans la durée afin de pouvoir anticiper l’avenir probable si rien est fait.

Le coût de la collecte

Le problème ici est que la collecte d’information et son stockage est très coûteux. William a illustré par un exemple assez simple mais très illustratif.


public class Client {
public static void main(String[] args) {
int NBTEST = 100;

long all = 0;
for (int i=0; i < NBTEST; i++) {
long start = System.nanoTime();
c1();
all += System.nanoTime() - start;
}
System.out.println("Mean: " + all / NBTEST);
}

public static void c1() {
}
}

L’objectif de ce code est de mesurer le temps d’exécution de l’appel à c1().

Sur mon PC portable assez lent, on obtient une moyenne de l’ordre de 1,5 ms.

Mais quel est le coût de cette mesure ?

Pour cela, William ajoute simplement (et seulement), deux appels à nanoTime() dans la méthode c1() qui devient donc :


public static void c1() {
System.nanoTime();
System.nanoTime();
}

La moyenne passe à environ 3 ms sur la même machine. On peut facilement conclure que le temps de la collecte est assez important.

Imaginez maintenant ce qui peut se passer lorsque les traitements s’empilent :


public static void c1() {
System.nanoTime();
c2();
System.nanoTime();
}

public static void c2() {
System.nanoTime();
c3();
System.nanoTime();
}

public static void c3() {
System.nanoTime();
c4();
System.nanoTime();
}

public static void c4() {
System.nanoTime();
System.nanoTime();
}

Notre système de mesure va introduire une gène dans la capacité de nos JVM à réaliser les vrais traitements pour les utilisateurs. Ici en l’occurrence, la méthode c4() ne fait toujours rien et pourtant nous avons consommé du temps ! Imaginez qu’il faille en plus persister les données collectées dans un fichier de log ou une base ou que sais-je encore !

L’idée développée par William est qu’il faut tenter de minimiser cet effort de collecte. Comment ?

Tracer l’utile seulement

La première piste pourrait être de ne tracer que là où on constate des ralentissements. Par exemple, se placer à une couche interface (servlet) que l’on peut imaginer être notre méthode c1() et mesurer tous les temps de chaque niveau c1() de notre application. Puis, lorsqu’on constate un incident, on regarde quel c1() a posé problème et alors, dans ce cas, on ajoute du code sur tous les niveaux c2() sollicités par ce c1(). On relivre l’application en production et on attend à nouveau que cela se dégrade puis ainsi de suite jusqu’à identifier le problème au niveau le plus fin.

Évidemment, cette méthode n’est pas envisageable dans un contexte de production !

Le crédit-temps pour les traces

L’autre piste consiste plutôt à s’attribuer une sorte de « crédit-temps » de perturbation et à dynamiquement adapter les mesures à chaque itération de collecte. Cela signifie que pour la première itération, on va s’attacher à évaluer un c1() en dessous, un c2() et un c3(). Puis, voyant notre crédit-temps épuisé, on va stopper là la collecte pour cet appel. A l’appel suivant, on partira du même c3() puis on ira vers c4() et on prendra un autre c1(). Et ainsi de suite. A bout de quelques itérations, nous devrions avoir les mesures pour l’ensemble de notre système.

L’axe du temps est donc utilisé pour collecter de la donnée et identifier les goulots d’étranglement avec un temps de nuisance maîtrisé !

Une amélioration possible consiste à essayer d’éliminer le bruit inutile de notre collecte. En effet, si nous savons par expérience qu’un traitement (niveau c2() par exemple) est de l’ordre de 1 ms en moyenne, et que lors d’une mesure niveau c2(), nous constatons que nous avons moins de 1,2 ms par exemple, il est alors inutile d’aller collecter les données dans les sous-niveaux puisque nous savons déjà que tout se déroule correctement.

A l’inverse, si notre c2() commence à dériver dans le temps, il va être intéressant de collecter l’information sur ses sous-niveaux pour lesquels nous appliquerons la même stratégie de collecte.

Au bout de quelques itérations, la collecte devrait être ciblée vers les points « anormaux » de notre traitement et donc, être efficace à 99 % !

Si nous arrivons à identifier les méthodes coûteuses, il est ensuite plus aisé de demander aux développeurs d’optimiser ces parties de code. Optimiser ne veut pas dire nécessairement d’améliorer la dite méthode mais peut-être de regarder au dessus si l’appel est véritablement nécessaire.

Par exemple, si nous reprenons notre exemple, l’appel à la méthode c4() est sans doute inutile puisque c4() ne fait rien ! On ne peut pas optimiser d’avantage c4() mais on peut se passer de son appel !

C’est le même principe par exemple pour une requête SQL. La requête peut-être optimisée mais jusqu’à un certain stade seulement. Pour améliorer ensuite, il faudra regarder à un niveau plus haut dans la couche des services.

Adapter le logiciel

Une fois la collecte réalisée, nous pouvons tenter de nous en servir pour auto-adapter notre logiciel à son environnement.

Par exemple, William prend le cas d’un utilisateur qui arrive sur un site assez chargé. Il ouvre la première page et patiente une dizaine de secondes. Il ouvre une seconde et une troisième et attend à chaque fois une dizaine de secondes. De fait, il se dit que ce site « craint ».

L’idée serait alors de fabriquer des lignes prioritaires (comme les FAST LANE dans les aéroports) de façon à ce qu’un utilisateur qui a déjà patienté une fois voit son prochain traitement prioritaire par rapport à d’autres n’ayant jamais patienté.

Pour cela, il va être nécessaire d’être capable d’allouer la puissance de traitement dynamiquement en fonction du contexte. Ce qui veut dire ralentir certains traitements au profit d’autres (on peut imaginer des sleeps par exemple).

A la manière de ce qu’on connaît avec la gestion de projet et le fameux « chemin critique », on peut identifier dans certains traitements le chemin critique qui implique que tout retard dans l’un de ses composants entraînera un retard total de la chaîne. Un Thread n’a pas cette notion. Lorsqu’on lui demande un traitement, il va tenter de l’exécuter le plus vite possible sans tenir compte du contexte environnant. Or, il peut ne pas être sur le chemin critique et peut s’exécuter plus tardivement.

Conclusion

William a donc exposé pendant près de deux heures la problématique de la collecte d’information sur l’état de la JVM et proposé des pistes pour adapter nos logiciels à cet état plutôt que d’observer impuissants la dégradation de nos JVM (se soldant en général par un arrêt-relance un peu brutal).

Les outils d’instrumentation que sa société propose permettent de répondre à ces besoins : Voir le site www.jinspired.com. Attention, le coût n’est pas anodin (à partir de 5k€ par CPU et licence globale pour un site à 115k€) sur lesquels il faut ensuite ajouter un coût de maintenance.

Toutefois, le rapport coût / efficacité peut s’avérer intéressant sur du long terme et peut-être grâce au tuning live vous faire économiser l’acquisition de quelques machines supplémentaires pour tenir la charge ! A étudier donc.

Tester unitairement la performance en Java

L’importance accordée aux tests lors du développement joue un rôle primordial dans la réussite et la qualité d’une application. Si dans bon nombre d’applications il existe des tests unitaires, les tests de performance sont souvent absents ou interviennent tardivement (bien souvent lorsque des problèmes commencent à être remontés).
Pour diagnostiquer au plus tôt ces problèmes, il existe des outils permettant de tester unitairement la performance. Parmi ceux disponibles, j’ai retenu ContiPerf.

Petit tour d’horizon des fonctionnalités offertes par l’outil.

La performance des tests fonctionnels

Vous avez un projet Java (ou avec une autre technologie Web). Si dans votre projet, vous avez des scénarios Selenium pour vous assurer que votre projet ne présente pas de régressions fonctionnelles, c’est bien, vous êtes un bon élèves. Si en plus, vos tests sont automatisés par une plate-forme d’intégration continue (du type Jenkins), alors vous méritez une mention honorable. Mais est-ce que vous avez pensé à collecter des données de performance pendant l’exécution de ces tests ?

Un outil à la rescousse

Pour nous aider dans cette tache, dynaTrace met à notre disposition un outil gratuit qui peut facilement se glisser une plate-forme existante de tests Selenium. L’outil en question se nomme « dynaTrace Ajax Edition » et vous pouvez le trouver à l’adresse suivante : http://ajax.dynatrace.com/ajax/en/

Lorsqu’on lance à la main l’outil, on alors la possibilité d’exécuter le navigateur de son choix et l’outil capturera, au cours d’une session d’analyse, toutes les requêtes qui passe par le navigateur et rendra compte du temps passé dans le réseau, celui passé dans le rendu de la page ou dans les scripts :

Chaque session est ensuite archivée. C’est donc une vision assez complète de la performance de la requête du point de vue client que nous obtenons. Si vous êtes en train de tester en lisant mes explications, vous allez surement me dire que c’est bien joli tout ça mais comment peut-on faire pour que les données soient collectées pendant l’exécution de mes tests Selenium. Rien ne l’empêche car dans les faits, dynaTrace Ajax Edition est un plugin ajouté au navigateur, il est constament présent (même lorsqu’on lance le navigateur en dehors d’une session de l’outil). Il suffit de savoir communiquer avec ce plugin ; et le langage disponible est la variable d’environnement. Pour ma part, j’en utilise 3 :

  • DT_AE_AGENTACTIVE       # active la collecte des données par dynaTrace Ajax Edition ; à noter que l’outil doit être lancé pour agréger les données capturées par le plugin
  • DT_AE_AGENTNAME    # donne un nom aux données collectées
  • DT_AE_CLEARCACHE    # nettoie le cache avant de lancer les requêtes ou non

Dans la pratique avec Selenium, vous avez surement un script de lancement votre Remote Control qui ressemblera à :

set DT_AE_AGENTACTIVE=true
set DT_AE_AGENTNAME=IDEO_SELENIUM
set DT_AE_CLEARCACHE=true
ant -Dport=5557 -Dhost=localhost -DhubURL=http://localhost:4444 -Denvironment="ie 8 windows" launch-remote-control

Dorénavant, à chaque fois que le Remote Control lancera un navigateur et que dynaTrace Ajax sera démarré, les données de performance seront archivées dans l’outil :

Les inconvénients

C’est prometteur mais ce n’est pas parfait. En tout cas, personnellement, j’y vois quelques inconvénients :

  • Tout d’abord une limitation de la version gratuite : seul les navigateurs Internet Explorer 8, 9 et 10 et Firefox 9 et 10 sont officiellement supportés. Si vous voulez supporter de plus ancienne version, il faudra soit regarder du côté de la version payante, soit rechercher d’autres outils
  • Ce n’est pas une solution centralisée : en effet, les données ne sont visibles et archivées que sur le client lourd sur le poste où le client web s’exécute. Dans le cas d’un Grid Selenium, où il y a certainement autant de machines (certainement virtuelles) que de versions de navigateur à tester, il y aura autant de client lourd dynaTrace à lancer.
  • Le suivi des performance n’est pas aisé : ce qui est principalement interressant avec ce genre de mesure, c’est d’avoir un suivi des mesures pour voir rapidement les dégradations induites par les nouveaux développements. Ici, je ne trouve pas ça évident. Tout d’abord, le nom des données archivées est celui données dans la variable d’environnement DT_AE_AGENTNAME. Il n’est donc déterminé qu’au moment de lancer le Remote Control, autant dire que toutes les archives porteront le même nom (que ce soit pour tester l’affichage d’une liste ou d’un message d’alerte). De toute façon, la comparaison n’est pas facile, il faut ouvrir les archives prendre les mesures à la main et les comparer soit même.

Un début de solution

Nous ne sommes pas pour autant totalement dépourvu si on décide de prendre le temps de développer ses propres outils. Avec l’aide de quelques paramètres, dynaTrace peut émettre une requête HTTP avec un contenu JSON synthétisant les données enregistrées pendant une session d’analyse. Avec une simple servlet et un parser du type google-gson, il est alors possible de récupérer les valeurs et de les archiver soi-même de manière centralisée.

Les paramètres sont à ajouter dans le fichier dtajax.ini dans le répertoire d’installation de dynaTrace :

-Dcom.dynatrace.diagnostics.ajax.beacon.uploadurl=http://localhost:8080/beaconstorage/endpoint
-Dcom.dynatrace.diagnostics.ajax.beacon.portalurl=http://localhost:8080/beaconstorage/endpoint
-Dcom.dynatrace.diagnostics.ajax.beacon.autoupload=true

Et la requête JSON reçu alors par votre Servlet (ou autre technologie) sera de la forme :

{
 "version":"2.0.0",
 "url": "www.mydomain.com",
 "rank":95,
 "ranks":{
 "cache": {"rank":100, "comment":"" },
 "net": {"rank":98, "comment":"" },
 "server": {"rank":94, "comment":"2 dynamic server-requests" },
 "js": {"rank":89, "comment":"4 slow JS handlers"}
 },
 "timetoimpression":1587,
 "timetoonload":1645,
 "timetofullload":2747,
 "reqnumber":9,
 "xhrnumber":0,
 "pagesize":264562,
 "cachablesize":0,
 "noncachablesize": 264562,
 "timeonnetwork": 400,
 "timeinjs": 30,
 "timeinrendering":200
}

Vous trouverez de plus ample information sur le site officiel de dynaTrace bien sûr : http://ajax.dynatrace.com/ajax/en/ mais aussi sur le blog officiel du produit : http://blog.dynatrace.com/2010/10/30/web-performance-optimization-use-cases-part-3-automation/