Programmation GWT 2 : Sous le capot de GWT
Date de publication : 10/12/2009 , Date de mise à jour : 10/12/2009
Par
Sami Jaber (Home)
Avant-propos
I. Introduction au compilateur
I.1. Vive les fonctions JavaScript !
II. Les étapes du compilateur
II.1. Lecture des informations de configuration
II.2. Création de l'arbre syntaxique GWT
II.3. La génération de code JavaScript et les optimisations
II.3.a. La réduction de code (pruning)
II.3.b. La finalisation de méthodes et de classes
II.3.c. La substitution par appels statiques
II.3.d. La réduction de type
II.3.e. L'élimination de code mort
II.3.f. L'inlining
III. Tracer les optimisations
IV. Les options du compilateur
V. Accélérer les temps de compilation
VI. Les linkers
VII. La pile d'erreurs en production
VII.1. Table des symboles
VIII. Liens
Avant-propos
Toute l'ingéniosité de GWT est d'avoir su construire un compilateur Java vers Java-
Script. Un compilateur intelligent capable d'optimiser et de générer du code tout en
respectant les préceptes de base du Web. Toute cette face cachée de GWT est encore
très méconnue du grand public, qui, après tout, n'a pas à se soucier des implémentations
internes du compilateur. Et pourtant, les vraies pépites, la vraie beauté de ce
framework réside dans cette partie passionnante de GWT. Pour celui qui sait
décrypter un minimum les nombreuses subtilités et configurations du compilateur,
chaque fonctionnalité est une source d'inspiration unique.
Le compilateur GWT est en perpétuelle évolution car la taille du framework ne cesse
d'augmenter (il suffit d'observer le nombre de nouvelles API). Les utilisateurs n'ont
de cesse de réclamer des applications réactives avec des temps de chargement
instantanés ; impossible dans ce contexte de s'assoir sur ses lauriers. Plus qu'une
nécessité, l'amélioration du JavaScript généré par le compilateur est devenue pour
chaque version une urgence vitale.
Ce chapitre explore les nombreuses facettes du compilateur GWT et aborde la structure
des fichiers générés et les différentes optimisations. Un éclairage particulier est
apporté au mécanisme permettant d'étendre le processus de génération pour y
ajouter des traitements spécifiques.
I. Introduction au compilateur
Le compilateur de GWT est l'essence même du framework. Lorsqu'on sait la
richesse des sept mille classes du JDK, on réalise que celui qui ose imaginer un jour
que ce langage peut produire du code JavaScript a du génie.
Même s'il est vrai qu'il existe des similitudes entre Java et JavaScript, certaines subtilités
peuvent poser problème. Java propose des classes, JavaScript des prototypes de
méthodes. Java dispose d'un mécanisme d'espaces de nommage (les fameux packages),
JavaScript non. Java permet d'effectuer des appels polymorphiques, ils sont
plus complexes en JavaScript. Java est un langage à typage statique, JavaScript un
langage à typage dynamique.
Malgré tous ces points, GWT réussit à marier ces deux langages sans briser à aucun
moment leur sémantique respective.
Pour bien comprendre les subtilités du compilateur, analysons ce qu'il génère sur des
cas triviaux.
I.1. Vive les fonctions JavaScript !
Prenons la classe Animal suivante. Elle contient un constructeur avec deux paramètres,
deux variables membres et une méthode parle(). Nous invoquons dans la
méthode onModuleLoad(), la méthode parle(), puis nous affichons une des deux
propriétés par le biais d'une alerte JavaScript.
En Java, cela donne :
package com.dng.client;
public class Animal {
String race ;
public String getRace() {
return race;
}
public void setRace(String race) {
this.race= race;
}
int age ;
public Animal(String race, int age) {
super();
this.race= race;
this.age = age;
}
public void parle() {
};
}
public class CompilateurSample {
public void onModuleLoad() {
Animal a = new Animal("berger allemand",2);
a.parle();
Window.alert(a.getRace());
}
}
|
Pour le compiler en JavaScript, nous utilisons le script Ant généré par GWT lors de
la création du squelette projet. Ce script contient une tâche gwtc à laquelle nous
ajoutons les options de compilation suivante –draftCompile et –style PRETTY. La
première demande au compilateur de ne pas trop optimiser le script cible. En production,
cette option est à proscrire, car elle a tendance à générer un gros fichier. En
revanche, pour des raisons pédagogiques, cela permet d'obtenir une version fidèle du
JavaScript avant optimisation.
La seconde option demande à GWT d'afficher un fichier source JavaScript lisible
non obfusqué. Cela permet de mieux comprendre le contenu du script.
<target name="gwtc" depends="javac" description="GWT compile to JavaScript">
<java failonerror="true" fork="true" classname="com.google.gwt.dev.Compiler">
<classpath>
<pathelement location="src" />
<path refid="project.class.path" />
</classpath>
<jvmarg value="-Xmx256M" />
<arg value="-style" />
<arg value="DETAILED" />
<arg value="-draftCompile" />
<arg value="com.dng.CompilateurSample" />
</java>
</target>
|
Voici le résultat généré par le compilateur après suppression de quelques méthodes
techniques pour plus de clarté :
<script>
var _;
function nullMethod(){}
function $$init(){}
function $Object(this$static){
$$init();
return this$static;
}
function Object_0(){}
_ = Object_0.prototype = {};
function $$init_0(){
}
function $Animal(this$static, race, age){
$Object(this$static);
$$init_0();
this$static.race = race;
this$static , age;
return this$static;
}
function $getRace(this$static){
return this$static.race;
}
function $parle(){}
function Animal(){}
_ = Animal.prototype = new Object_0;
_.race = null;
function $$init_1(){}
function $CompilateurSample(this$static){
$Object(this$static);
$$init_1();
return this$static;
}
function $onModuleLoad(){
var a;
a = $Animal(new Animal, 'berger allemand', 2);
$parle();
alert_0($getRace(a));
}
function CompilateurSample(){}
--></script>
|
Quand on s'attarde un instant sur le contenu de ce script, on comprend à quel point
il est difficile de traduire en JavaScript les concepts objet existant dans le monde Java.
La première chose à noter est l'absence totale de démarcation objet. En JavaScript,
tout est fonction ! Et GWT l'a bien compris.
Dans le listing précédent, la méthode onModuleLoad() est bien présente. Celle-ci
commence par créer un objet de type $Animal lors de l'étape (1). Dans le monde de
l'orienté objet, une classe dérivant d'une superclasse appelle toujours le constructeur
de sa classe mère lors de sa propre construction (2). C'est bien le cas dans le code
JavaScript : l'étape (3) invoque le constructeur de la classe Object.
Enfin, les étapes (4) et (5) invoquent respectivement les méthodes parle() et
getRace(). Malgré certains détails (comme la présence systématique du mot-clé
this$static dans chaque fonction), ce code JavaScript est limpide.
II. Les étapes du compilateur
Nous allons ici détailler les différentes étapes menant à la génération des artéfacts
lors de la compilation d'un programme GWT. L'idée est ici de bien comprendre le
processus d'optimisation et le modèle interne au compilateur.
GWT a besoin du code source Java.
Il y a un débat récurrent au sein des contributeurs GWT qui est celui du modèle de
compilation. À l'origine, le choix a été de s'appuyer sur le code source Java pour
générer le JavaScript plutôt que réutiliser le byte code. Nous ne discuterons pas ici de
la pertinence de cette décision. Il faut simplement savoir que GWT a besoin du code
source car il extrait certaines informations telles que le code JSNI. Une autre raison
avancée par les créateurs de GWT est la possibilité d'optimiser à partir d'informations
présentes uniquement dans le code source. Si on prend, par exemple, les types
génériques (Class<T>), ceux-ci sont réécrits par le compilateur.
Voyons maintenant les différentes étapes intervenant lors de la compilation.
II.1. Lecture des informations de configuration
La première étape consiste à charger récursivement les informations contenues dans
les fichiers de configuration de tous les modules. Cela inclut le module compilé mais
également l'ensemble de ses dépendances.
Tous les paramètres liés aux différentes permutations de la liaison différée (propriétés,
conditions, types), mais également les éventuels scripts embarqués (ceux qui
définissent les valeurs des propriétés) sont analysés.
À partir de métadonnées extraites lors de ce parcours, GWT va générer les différentes
permutations.
II.2. Création de l'arbre syntaxique GWT
La seconde étape consiste à parcourir récursivement l'ensemble des modules dépendant
du module que l'on souhaite compiler pour construire une sorte d'arbre syntaxique
en mémoire. Un arbre syntaxique est un modèle objet spécifique qui a pour
but de représenter un programme source au travers des différentes structures de contrôle
qui le composent.
En Java, il existe une sorte de standard dans ce domaine, le JDT (Java Development
Tool) issu de l'IDE Eclipse. Comme le montre la figure suivante, il est possible de se
servir du JDT pour créer un arbre syntaxique. JDT propose une API riche constituée
des types IPackageFragment, ICompilationUnit, IType, IMethod, etc.

L'outil JDT (Java Development Tool)
Si les concepteurs de GWT avaient fait le choix de générer du code JavaScript à
partir du source Java, l'arbre JDT aurait constitué un candidat pour construire un
arbre syntaxique (arbre AST) automatiquement.
Mais au lieu de cela, il a fallu créer de zéro un modèle spécifique adapté aux contraintes
de GWT (fonctions JSNI, liaison différée, etc.). Lors du parcours récursif, le
compilateur examine le code source Java et construit en mémoire un arbre AST de
type JProgram. Le schéma suivant illustre globalement le concept : une méthode est
composée d'un ou plusieurs blocs de programmes, chaque bloc est lui-même composé
de variables, d'expressions ou de structures de contrôles (condition, boucle,
etc.). Bref, en fin d'analyse, GWT dispose en mémoire d'un arbre (potentiellement
énorme) composé de l'ensemble des modules de l'application (notez au passage
l'intérêt de réduire les dépendances).

Structure de l'arbre AST GWT
Une fois cet arbre construit (qu'on nomme arbre « unifié », car il n'est lié à aucune
permutation), le compilateur réalise une précompilation. C'est-à-dire qu'il substitue
et génère toutes les classes Java à partir des règles définies dans les métadonnées
(substitution de classe, génération de code, etc.), l'objectif étant d'obtenir un arbre
syntaxique unifié auquel on accole des sous-arbres spécifiques. Notez que, lors de
cette opération, des optimisations interviennent déjà car certaines règles peuvent
conduire à des permutations semblables.
 |
GWT ne génère pas de permutations d'arbre
À aucun moment, GWT ne génère n permutations en mémoire d'un arbre syntaxique ; ce serait trop
lourd à gérer d'un point de vue des performances. Il crée un seul arbre AST unifié puis lui associe des
métadonnées correspondant aux différentes règles de substitution.
|
Une fois ces conditions réunies, la compilation Java vers JavaScript proprement dite
peut commencer.
II.3. La génération de code JavaScript et les optimisations
Pour chaque permutation, GWT fusionne l'arbre unifié et les sous-arbres spécifiques à
chaque fois qu'il tombe sur l'instruction GWT.create(). On pourrait penser que la génération
est finalement une banale conversion de code Java vers JavaScript mais il n'en est
rien. Une telle conversion aurait conduit GWT à générer un fichier JavaScript de plusieurs
dizaines de méga-octets, vu la taille initiale du framework GWT. Impensable.
En réalité, lors de cette étape, un inexorable processus d'optimisation démarre. Un processus
montré du doigt par des milliers (peut-être un jour des millions) de développeurs
GWT à travers le monde. Les temps de compilation de GWT sont longs, et ce, parfois
horriblement. Mais lorsqu'on découvre ce qui se cache derrière cette phase indispensable,
on en veut instinctivement moins à GWT et on prend son mal en patience.
Il n'y a de meilleur exercice pédagogique que d'expliquer GWT à travers son code
source. Voici la méthode compilePermutation() de la classe interne
JavaToJavaScriptCompiler, exécutée lorsque l'utilisateur déclenche une compilation.
C'est sûrement l'une des méthodes les plus importantes du framework. Toutes
les étapes de la compilation y sont illustrées, jugez-en par vous-même :
public static PermutationResult compilePermutation(..., int permutationId){
long permStart = System.currentTimeMillis();
AST ast = unifiedAst.getFreshAst();
(...)
ResolveRebinds.exec(jprogram, rebindAnswers);
if (options.isDraftCompile()) {
draftOptimize(jprogram);
} else {
do {
boolean didChange = false;
didChange = Pruner.exec(jprogram, true) || didChange;
didChange = Finalizer.exec(jprogram) || didChange;
didChange = MakeCallsStatic.exec(jprogram) || didChange;
didChange = TypeTightener.exec(jprogram) || didChange;
didChange = MethodCallTightener.exec(jprogram) || didChange;
didChange = DeadCodeElimination.exec(jprogram) || didChange;
if (isAggressivelyOptimize) {
didChange = MethodInliner.exec(jprogram) || didChange;
}
} while (didChange);
}
(...)
(...)
JavaToJavaScriptMap map = GenerateJavaScriptAST.exec(jprogram, jsProgram,
options.getOutput(), symbolTable);
if (options.isAggressivelyOptimize()) {
boolean didChange;
do {
didChange = false;
didChange = JsStaticEval.exec(jsProgram) || didChange;
didChange = JsInliner.exec(jsProgram) || didChange;
didChange = JsUnusedFunctionRemover.exec(jsProgram) || didChange;
} while (didChange);
}
JsStackEmulator.exec(jsProgram, propertyOracles);
SoycArtifact dependencies = splitIntoFragment(logger, permutationId,
jprogram, jsProgram, options, map);
Map<JsName, String> obfuscateMap = Maps.create();
switch (options.getOutput()) {
case OBFUSCATED: (...)
case PRETTY: (...)
case DETAILED: (...)
}
PermutationResult toReturn = generateFinalOutputText(logger, permutationId,
jprogram, jsProgram, options, symbolTable, map, dependencies,
obfuscateMap, splitBlocks);
System.out.println("Permutation took "
+ (System.currentTimeMillis() - permStart) + " ms");
return toReturn;
}
|
Explicitons un peu ce morceau de code.
La première action consiste à résoudre les instructions GWT.create(). Puis le compilateur
extrait l'option draftCompile qui indique si l'utilisateur souhaite réduire le
nombre d'optimisations au profit de la vitesse de compilation. Le JavaScript généré
dans ce mode est plus volumineux mais les temps de compilation moindres.
En mode normal, la totalité des optimisations est appliquée de manière séquentielle
tel un citron pressé jusqu'à ne plus pouvoir extraire une seule goutte. Mais pourquoi
réexécuter les optimisations de manière quasi infinie ? Ce qui est vrai à l'instant t ne
l'est plus à l'instant t+1. Certaines optimisations ont pour effet de détacher de l'arbre
AST certaines classes qui deviennent aussitôt candidates à la réduction (pruning).
D'où l'intérêt d'itérer constamment.
Voici les étapes intervenant dans la compilation et invoquées par la fonction précédente compilePermutation() :
- la réduction de code (pruning) ;
- la finalisation de méthodes et de classes ;
- la substitution par appels statiques ;
- la réduction des types ;
- l'élimination de code mort ;
- l'inlining.
II.3.a. La réduction de code (pruning)
La réduction de code est un procédé qui permet de supprimer toutes les classes,
méthodes, champs et paramètres non utilisés dans une application.
Pour se faire une idée précise de l'efficacité du pruning, établissons un parallèle avec un
autre framework, celui du SDK Java. Ce framework contient plus de 7 500 classes et il faut
savoir qu'en moyenne l'immense majorité des projets Java utilisent environ 5 à 10 % des
classes du SDK. En pratique, cela signifie qu'il est possible de produire un JRE de 1Mo.
Sun a d'ailleurs déjà commencé à oeuvrer dans ce sens avec le framework RIA JavaFX.
Si la réduction de code n'existe pas dans le monde Java à proprement dit, cela est dû en
partie à l'instanciation dynamique de classes. Avec Java, il est possible à tout moment
de créer des instances via le mécanisme de réflexion (Class.forName("classe") ou
class.newInstance()). Cette possibilité met fin de facto à toute velléité d'optimisation
via un framework allégé.
Or, GWT prend comme hypothèse que le chargement dynamique de classes est
interdit. Le compilateur a besoin de maîtriser l'ensemble de l'arbre syntaxique ainsi
que les tenants et aboutissants du code source. Dans ce contexte, le pruning apporte
un gain considérable car il supprime ainsi les dizaines de classes présentes dans le
JRE émulé de GWT. Aussi minime soit-il, sans le mécanisme de réduction, le fichier
JavaScript généré pourrait s'élever à plusieurs dizaines de méga-octets dans une
application métier complexe. Chose inconcevable.
Voici à titre d'exemple un code source Java et le résultat JavaScript opéré après pruning.
public class Animal {
String race ;
int age ;
public String getRace() {
return race;
}
public void setRace(String race) {
this.race= race;
}
public int getAge() {
return age;
}
public void setAge(int race) {
this.age= age;
}
public Animal(String race, int age) {
super();
this.race= race;
this.age = age;
}
public void parle() {
};
}
public class CompilateurSample implements EntryPoint {
public void onModuleLoad() {
Animal a = new Animal("berger allemand",2);
Window.alert(a.getRace());
}
}
|
Dans le code précédent, nous n'utilisons que la propriété race de la classe Animal.
Voici ce que GWT génère au niveau JavaScript :
_ = Object_0.prototype = {};
function $Animal(this$static, race){
this$static.race = race;
return this$static;
}
function Animal(){}
_ = Animal.prototype = new Object_0;
_.race = null;
function init(){
var a;
a = $Animal(new Animal, 'berger allemand');
$wnd.alert(a.race);
}
|
Le résultat est épatant. On ne trouve plus aucune trace de la propriété age, comme si
elle n'avait jamais existé dans le code source. Le champ et les méthodes getAge() ou
parle() et le paramètre du constructeur ont disparu !
Vous découvrez là une des plus-values les plus importantes de GWT et comprenez
par la même occasion pourquoi le chargement dynamique de classe est interdit.
II.3.b. La finalisation de méthodes et de classes
L'approche objet a pour principal avantage de fournir des mécanismes évolués tels
que l'héritage et la redéfinition mais souvent au dépend d'une complexité d'exécution
accrue. Le simple fait de marquer une méthode finale en phase de développement
permet au compilateur d'insérer (inlining) son contenu. Dans la pratique, cela consiste
à supprimer la méthode pour déplacer son code dans l'appelant (notamment
pour les variables). L'inlining libère de la mémoire en évitant de maintenir à l'exécution
une pile d'appels et permet parfois d'économiser quelques lignes de code.
Globalement, il est d'ailleurs recommandé en Java de marquer systématiquement une
méthode ou une classe du mot-clé final si on est certain qu'elle ne sera pas redéfinie.
II.3.c. La substitution par appels statiques
Même si ce n'est pas une optimisation en soit, la substitution statique modifie le code
pour faciliter les prochaines optimisations.
Cette tâche consiste à rechercher tous les appels non polymorphiques (c'est-à-dire
mettant en oeuvre une méthode non redéfinie) pour réécrire l'appel en passant par
une méthode statique. Le paramètre de cette méthode statique est l'instance sur
laquelle on souhaite invoquer la méthode.
Au premier abord le principe peut paraître un peu tordu mais les bénéfices sont précieux.
Voici un exemple :
Animal instance = new Animal();
instance.parle()
function $Animal(instance){
parle(instance);
}
function parle(instance){
(...)
}
|
Comparée à une version polymorphique, la version statique présente l'avantage de
mettre à plat les appels de méthodes en les transformant en fonctions basiques Java-
Script. Par ailleurs, cela facilite non seulement les suppressions lorsqu'une opération
de réduction intervient (car on sait précisément qui appelle quoi) mais permet de
gagner des octets en réécrivant le mot-clé this par une chaîne plus courte. Il fallait
simplement y penser !
II.3.d. La réduction de type
La réduction de type ou Type Tightening fait partie des autres optimisations géniales
du compilateur GWT.
Pour résumer le principe en quelques mots, le compilateur infère les types les plus
spécifiques pour mieux supprimer les types abstraits.
Comme tout bon développeur qui se respecte, nous utilisons régulièrement les
grands principes de la programmation objet, les interfaces, les types abstraits ou la
généricité pour instaurer une forme de découplage entre classes. Une fois le type concret
créé, il est d'usage de préférer l'interface List à la classe ArrayList ou l'interface
Map à la classe HashMap.
Le compilateur GWT ayant pour objectif principal la réduction de code, toute abstraction
va progressivement disparaître du source Java pour ne laisser place qu'à des types
concrets. Ce processus commence par l'inspection des variables locales, des champs,
des paramètres et des types de retour puis se poursuit par l'analyse de l'arbre syntaxique
pour analyser les changements intervenant sur un type (appelés également type flow).
Lorsqu'un type ArrayList est créé à la base, toutes les références de type List sont
transformées en références de type ArrayList avec pour effet immédiat d'alléger par
le pruning le code généré via la suppression des types abstraits.
Certaines variables peuvent également prétendre à être réduites, notamment celles
qui n'ont jamais été initialisées ou celles qui contiennent null. Ces dernières ouvrent
la voie à de nombreuses optimisations possibles.
Après la réduction généralisée de type réalisée, le compilateur effectue la même opération
sur les appels de méthode polymorphique. Lors de cette étape, l'ambiguïté
(quelle méthode de quelle classe appeler pour un héritage donné) qui existait avant la
réduction de type est levée.
II.3.e. L'élimination de code mort
L'élimination de code dit « mort » consiste à supprimer tout code inatteignable ou
toute expression invariante. Ainsi l'expression "x || true" sera par exemple remplacée
par "x".
Mais cette élimination va plus loin : elle remplace également les post-incrémentations
par des pré-incrémentations, plus performantes (cela évite de stocker une variable temporaire),
en s'assurant bien évidemment qu'aucun effet de bord n'est généré. L'élimination
vérifie également l'utilisation des conditions (switch case), optimise les calculs
sur les booléens, optimise les expressions de type try{} catch{}, supprime le code des
boucles à condition constante de type while (faux) { //code }, et supprime les successions
de break inutiles.
switch(i) { default: a(); b(); c(); } devient { a(); b(); c(); }
switch(i) { case 1: a(); b(); c(); } devient if (i == 1) { a(); b(); c(); }
switch(i) { case 1: a(); b(); break; default: c(); d(); } devient if (i == 1) { a(); b(); } else { c(); d(); }
II.3.f. L'inlining
Lorsqu'une méthode ne crée pas d'effet de bord sur des parties tierces de l'application
et que son contenu est suffisamment maîtrisé, le compilateur remplace la fonction
appelée par son code. Cela simplifie les optimisations ultérieures et épargne à
l'exécution une pile d'appels complexe.
Les méthodes getXX() ou setXX() sont souvent de bons candidats à l'inlining.
En voici un exemple :
Forme f = new Carre(2);
int a = f.getSurface()
Forme f = new Carre(2);
int a = f.length * f.length;
int a = 4;
|
Magique non ?
III. Tracer les optimisations
Le compilateur fournit une option très intéressante permettant de tracer les différentes
optimisations observées sur une méthode. Le mode de trace s'utilise en pointant
le nom d'une méthode de la manière suivante :
java com.google.gwt.dev.compiler -Dgwt.jjs.traceMethods=com.dng.sample.onModuleLoad
com.dng.CompilateurSample
|
Voici pour un exemple donné, le type d'affichage produit par l'option de trace.
public abstract class Animal {
String race;
int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Animal() { }
public Animal(String race, int age) {
this.race = race;
this.age = age;
}
public abstract String getRace();
}
public class Chien extends Animal {
String race = "BERGER ALLEMAND";
@Override
public String getRace() {
return race;
}
}
public class CompilateurSample implements EntryPoint {
public void onModuleLoad() {
Animal a = new Chien();
String race = a.getRace();
Window.alert("La race du client est " + race);
}
}
JAVA INITIAL:
---------------------------
public void onModuleLoad(){
Animal a = (new Chien()).Chien();
String race = a.getRace();
Window.alert("La race du client est " + race);
}
---------------------------
FinalizeVisitor:
---------------------------
public final void onModuleLoad(){
final Animal a = (new Chien()).Chien();
final String race = a.getRace();
Window.alert("La race du client est " + race);
}
---------------------------
JAVA INITIAL:
---------------------------
public static final void $onModuleLoad(CompilateurSample this$static){
final Animal a = (new Chien()).Chien();
final String race = a.getRace();
Window.alert("La race du client est " + race);
}
(...)
---------------------------
TightenTypesVisitor:
---------------------------
public static final void $onModuleLoad(CompilateurSample this$static){
final Chien a = Chien.$Chien(new Chien());
final String race = a.getRace();
Window.alert("La race du client est " + race);
}
---------------------------
PruneVisitor:
---------------------------
public static final void $onModuleLoad(){
final Chien a = Chien.$Chien(new Chien());
final String race = a.getRace();
Window.alert("La race du client est " + race);
}
---------------------------
RewriteCallSites:
---------------------------
public static final void $onModuleLoad(){
final Chien a = Chien.$Chien(new Chien());
final String race = Chien.$getRace(a);
Window.alert("La race du client est " + race);
}
---------------------------
InliningVisitor:
---------------------------
public static final void $onModuleLoad(){
final Chien a = (((()), new Chien()));
final String race = (("BERGER ALLEMAND"));
Window.alert("La race du client est " + race);
}
---------------------------
DeadCodeVisitor:
---------------------------
public static final void $onModuleLoad(){
final Chien a = new Chien();
final String race = "BERGER ALLEMAND";
Window.alert("La race du client est BERGER ALLEMAND");
}
---------------------------
CleanupRefsVisitor:
---------------------------
public static final void $onModuleLoad(){
new Chien();
"BERGER ALLEMAND";
Window.alert("La race du client est BERGER ALLEMAND");
}
---------------------------
DeadCodeVisitor:
---------------------------
public static final void $onModuleLoad(){
Window.alert("La race du client est BERGER ALLEMAND");
}
|
IV. Les options du compilateur
Le compilateur GWT est assez riche et ses options (comme la précédente) plutôt
méconnues du grand public. La version 2 apporte son lot de nouveautés avec l'introduction
du mode draftCompile et les rapports de compilation.
| Option |
Description |
| -logLevel |
Spécifie le niveau de trace : ERROR, WARN, INFO, TRACE, DEBUG, SPAM, ou ALL |
| -treeLogger |
Affiche les messages de sortie dans l'arbre du shell. |
| -workDir |
Spécifie le répertoire de travail du compilateur (doit être en écriture). Par défaut, pointe vers le répertoire temporaire du disque. |
| -gen |
Le répertoire contenant tous les fichiers sources générés par la liaison différée. |
| -ea |
Enable Assertion. Précise que le compilateur ne doit pas supprimer les
assertions créées en Java lors de la compilation (certaines personnes
préfèrent ne pas propager les assertions en JavaScript pour gagner en
performances et en taille). |
| -XshardPrecompile |
Optimisation qui va de paire avec les workers (permet d'éviter les
OutOfMemory durant la compilation de nombreuses permutations). |
| -XdisableClassMetadata |
Expérimental : désactive quelques méthodes de
java.lang.Class, notamment la méthode
Class.getName(), très coûteuse en JavaScript.
Cette option permet de gagner 5 à 10 % de code JavaScript généré. |
| -XdisableCastChecking |
Expérimental : désactive les vérifications de conversion à l'exécution
(accélère les temps d'exécution aux dépens de la sécurité du code). |
| -validateOnly |
Valide l'ensemble du code source mais ne le compile pas. Permet de
vérifier que la configuration des règles et propriétés de liaison différée
est correcte sans générer une compilation complète. |
| -draftCompile |
Réalise moins d'optimisations mais accélère la compilation. |
| -compileReport |
Active les rapports de compilation. |
| -localWorkers |
Spécifie le nombre de threads à utiliser par permutation (permet de
gagner en performance lorsque la compilation s'exécute sur une
machine multicoeur). |
| -war |
Le répertoire contenant les différents fichiers de permutation et la
racine du site. |
| -extra |
Le répertoire contenant les fichiers non déployés, appelés encore
extra. |
| -style |
Permet de produire du code JavaScript lisible ou crypté. |
Les options du compilateur
L'option -extra permet de générer des fichiers non déployés en production et contenant
diverses informations.
- Répertoire rpcPolicyManifest : un fichier de policy est un fichier de métadonnées détaillant l'ensemble des types RPC utilisés dans un module. Le fichier rpcPolicyManifest précise pour plusieurs modules les chemins relatifs de leur fichier de policy respectifs.
- Répertoire symbolMaps : contient une table de correspondances permettant de retrouver une classe à partir de son nom obfusqué dans le fichier JavaScript. Cette table sert essentiellement à l'affichage de la pile d'appels en cas d'erreur. Ce sujet est couvert en fin de chapitre.

Génération des fichiers avec l'option -extra
Notez que l'option –compileReport génère également des artéfacts dans le répertoire
war par défaut.
En plus des arguments classiques du compilateur, GWT utilise des propriétés système
pour certaines opérations avancées telles que le paramétrage des traces ou la suppression
des messages d'avertissement. Le tableau suivant illustre ces options système.
| Propriétés |
Description |
| Propriétés liées au compilateur |
| gwt.jjs.javaArgs |
Compilation parallèle : redéfinit des arguments au
sous-processus (exemple : -Xmx, -D, etc.). |
| gwt.jjs.javaCommand |
Compilation parallèle : redéfinit la commande permettant
de lancer une nouvelle machine virtuelle (par
défaut : $JAVA_HOME/bin/java). |
| gwt.jjs.maxThreads |
Compilation parallèle : le nombre maximal de threads
utilisés par processus. |
| gwt.jjs.permutationWorkerFactory |
Compilation parallèle : il existe deux modes pour lancer
la compilation, un mode multiprocessus avec plusieurs
JVM, et un mode multi-threads dans la même
JVM. Ce paramètre permet de préciser le mode souhaité. |
| gwt.jjs.traceMethods |
Génère des messages explicites quant à l'optimisation
d'une méthode. |
| Concerne le shell |
| gwt.browser.default |
Définit le navigateur à lancer par défaut lors de l'exécution
(supplante la variable système
GWT_EXTERNAL_BROWSER). |
| gwt.nowarn.webapp.classpath |
Supprime le message d'avertissement lorsque l'application
fait appel à des classes situées dans le
classpath système. |
| gwt.dev.classDump |
Demande au compilateur d'écrire toute classe instrumentée
lors de la phase de compilation sur disque
(utile par exemple pour analyser le contenu des types
JavaScriptObject). |
| gwt.dev.classDumpPath |
Toute classe Java réécrite lors de la phase de compilation
est générée dans ce répertoire |
| gwt.shell.endquick |
Ne demande pas de confirmation lors de la fermeture
du shell. |
| Traçabilité et gestion des versions |
| gwt.debugLowLevelHttpGet |
Affiche des informations de débogage lors des appels
Ajax. |
| gwt.forceVersionCheckNonNative |
Sous Windows seulement, utilise la version pure Java. |
| gwt.forceVersionCheckURL |
Permet de passer une URL personnalisée pour la version
de GWT. |
| JUnit |
| gwt.args |
Permet de passer des arguments au shell Junit. |
| com.google.gwt.junit.reportPath |
Spécifie le chemin de génération des rapports de benchmark. |
Options système (certaines sont peu documentées)
V. Accélérer les temps de compilation
Vu la complexité des optimisations et le mode de fonctionnement du compilateur, ce
n'est pas une surprise si l'une des préoccupations majeures des développeurs GWT
concernent les temps de compilation. Avec GWT 2 et la possibilité de tester le code
à partir d'un vrai navigateur, cette contrainte ne devrait plus réellement constituer un
facteur de blocage. En effet, dans cette version, la compilation réelle n'intervient que
pour valider définitivement le rendu visuel d'une application.
Malgré tout, il existe plusieurs optimisations possibles, dont certaines ont été abordées
dans le chapitre sur la liaison différée.
Voici une liste d'actions susceptibles de réduire en moyenne de 50 % le temps de compilation, voire 800 % dans certains cas :
- Réduire le nombre de permutations : GWT est paramétré par défaut pour générer 6 permutations (une par navigateur). Si on sait que seuls IE 7 et Firefox 3 seront supportés, il suffit de définir la propriété user.agent de la manière suivante dans le fichier de configuration du module : <set-property name="user.agent" value="ie6,gecko" />. Ce paramétrage réduit en moyenne de 50 % les temps de compilation.
- Ajouter l'option -draftCompile lors de la compilation : en phase de développement les gains peuvent aller de 5 à 30 % en fonction des scénarios.
- Donner une valeur à l'argument -localWorkers : GWT 1.5 a introduit la notion de worker pour la compilation. Pour une machine bi-coeur (Dual Core), une valeur paramétrée à 2 permet de paralléliser la compilation des permutations sur chaque coeur. Le gain est en moyenne de 10 %. Ce chiffre s'améliore progressivement dans le cas d'une machine à 4 coeurs (Quad Core).
- Configurer la JVM avec des paramètres adaptés : pour une application moyenne, les options suivantes permettent d'assurer suffisamment de mémoire, une taille de pile cohérente et un espace de stockage temporaire adapté.
java com.google.gwt.dev.Compiler -Xmx512M -Xss128k -Xverify:none -X:PermSize=32m Module
|
VI. Les linkers
Comme exposé précédemment, le processus de déploiement de GWT fait intervenir
un certain nombre d'étapes. La compilation génère des fichiers de permutation
placés dans un sous-répertoire du contexte web racine de l'application. Une fois la
page hôte appelée, un script de sélection est exécuté réalisant dans la foulée le chargement
de la permutation correspondant aux propriétés du navigateur.
Si ce mode de fonctionnement convient dans la majeure partie des cas, il est quelquefois
utile de s'insérer dans le processus de génération des fichiers pour y apporter
quelques modifications. Imaginez par exemple un site GWT déployé sous la forme
d'un portlet ou un gadget GWT proposant des métadonnées avec un point d'entrée
spécifique. De simples contraintes de déploiement (DMZ, répertoires spécifiques
par ressources, etc.) requièrent parfois la modification des fichiers générés par GWT.
Dans l'exemple suivant, qui représente le contrat d'un gadget GWT, la méthode
d'initialisation n'est pas onModuleLoad() mais init() :
@ModulePrefs(
title = "Gadget GWT",
directory_title = "Mon gadget GWT ",
screenshot = "gadget.png",
thumbnail = "thumb.png",
...
height = 210)
public class MyGWTGadget extends Gadget<MyGWTGadgetPreferences>
{
public void onModuleLoad() { }
protected void init(
final MyGWTGadgetPreferences prefs) {
...
}
}
|
Voici la séquence d'appel des différentes composantes de la construction GWT. À
chaque étape correspond un ensemble d'artéfacts générés ou compilés (les fichiers ou
morceaux de code source).

Fonctionnement des linkers
L'idée sous-jacente aux linkers est de permettre au développeur d'interférer dans le
mécanisme de construction et d'édition des différents liens pour y apporter un traitement
spécifique.
Il faut savoir que GWT propose par défaut cinq linkers définis dans le module
Core.gwt.xml situé dans le fichier gwt-user.jar.
- Le SingleScriptLinker : utilisé lorsqu'on souhaite générer un seul fichier Java-Script pour un module. Cela suppose qu'il n'existe qu'une seule permutation.
- Le XSLinker : utilisé lorsque le serveur hébergeant les permutations est différent de celui hébergeant la page hôte. Ce linker produit des fichiers d'extension xs comme <module>-xs.nocache.js.
- Le IFrameLinker : c'est le linker principal utilisé par GWT, il génère une IFrame cachée.
- Le SymbolMapLinker : ce linker se charge de créer un fichier de correspondances permettant d'afficher des piles d'erreurs pointant des noms de variables non obfusquées.
- Le SoycReportLinker : les rapports de compilation constituent l'historique de la compilation ; ce linker restitue des fichiers de traces contenant des métriques liées aux optimisations.
Voici un extrait de fichier de configuration (tiré du code source de GWT) illustrant
la déclaration d'un linker et son ajout dans le projet courant.
<module>
<inherits name="com.google.gwt.dev.jjs.intrinsic.Intrinsic" />
(...)
<super-source path="translatable" />
<define-linker name="sso"
class="com.google.gwt.core.linker.SingleScriptLinker" />
<define-linker name="std" class="com.google.gwt.core.linker.IFrameLinker" />
<define-linker name="xs" class="com.google.gwt.core.linker.XSLinker" />
<define-linker name="soycReport" class="com.google.gwt.core.linker.SoycReportLinker" />
<define-linker name="symbolMaps" class="com.google.gwt.core.linker.SymbolMapsLinker" />
<add-linker name="std" />
<add-linker name="soycReport" />
<add-linker name="symbolMaps" />
</module>
|
Ces linkers s'exécutent dans un ordre déterminé par trois états : avant
(LinkOrder.PRE), pendant (LinkOrder.PRIMARY) et après (LinkOrder.POST).
À titre d'exemple, le linker standard (IFrameLinker) qualifié dans le fichier
Core.gwt.xml"std" est un linker primaire, c'est-à-dire qu'il s'exécute de manière
autonome et produit un script de sélection chargeant les permutations dans une IFrame
cachée. Sa seule dépendance est celle liée à son héritage. IFrameLinker, au même titre
que la plupart des linkers primaires dérivent de la classe SelectionScriptLinker qui
lui offre la génération du script de sélection. Il est bien évidemment possible de redéfinir
à tout moment le comportement des linkers prédéfinis.
Voyons maintenant concrètement un exemple de linker personnalisé. Créer un linker revient à :
- Créer une classe dérivant de com.google.gwt.core.ext.Linker.
- Ajouter l'annotation @LinkOrder pour déterminer si le linker doit s'exécuter avant, après ou en remplacement du linker primaire. Le nombre de linkers n'est pas limité, seul le primaire est unique.
- Définir et ajouter le linker personnalisé dans le fichier de configuration du module (<module>.gwt.xml).
- Inclure dans le classpath du compilateur le nouveau linker.
Le linker suivant répertorie dans une chaîne de caractères l'ensemble des artéfacts
générés par le compilateur (permutation, images, etc.) suivi de la date de dernière
modification. Il ajoute ensuite un nouvel artéfact (un nouveau fichier) contenant
cette chaîne. L'objectif est de montrer ici un linker simple qui extrait des informations
du compilateur et crée de nouveaux fichiers en sortie.
package com.dng.linkers;
import java.util.Date;
import com.google.gwt.core.ext.LinkerContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.AbstractLinker;
import com.google.gwt.core.ext.linker.Artifact;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.core.ext.linker.EmittedArtifact;
import com.google.gwt.core.ext.linker.LinkerOrder;
@LinkerOrder(LinkerOrder.Order.POST)
public class MyLinker extends AbstractLinker {
public String getDescription() {
return "MyLinker";
}
public ArtifactSet link(TreeLogger logger, LinkerContext context,
ArtifactSet artifacts) throws UnableToCompleteException {
String artifactList="";
ArtifactSet toReturn = new ArtifactSet(artifacts);
for (Artifact artifact : toReturn) {
if (artifact instanceof EmittedArtifact) {
EmittedArtifact fic = (EmittedArtifact) artifact;
artifactList = fic.getPartialPath() + "," + new
Date(fic.getLastModified()).toString() + "\n" + artifactList ;
}
}
toReturn.add(emitString(logger, artifactList, "ListFiles.txt"));
return toReturn;
}
}
|
L'exécution du code précédent produit la sortie suivante.

Linker affichant la liste des artéfacts générés par le compilateur
La méthode link() est appelée par le compilateur qui lui transmet le paramètre
ArtifactSet, une collection de l'ensemble des artéfacts du module. Notez qu'un artéfact
n'est pas nécessairement un fichier physique généré dans le répertoire de destination du
module. Seules les ressources de type EmittedArtifact sont destinées à être générées.

Les différents types d'artéfacts
Dans le jargon des linkers, il existe plusieurs types d'artéfacts :
- la résultante d'une compilation (CompilationResult) contenant en mémoire des informations de permutation (flux JavaScript, clé MD5, etc.) ;
- les fichiers physiques (EmittedArtifact) générés dans le répertoire de sortie du module ( JavaScript, CSS, etc.) ;
- l'analyse de compilation (utilisée ensuite par les rapports de compilation).
Il est possible à tout moment de s'intercaler dans le processus de génération pour
modifier ces artéfacts. Différentes méthodes sont fournies dont certaines permettant
de lire le JavaScript généré, de le modifier ou simplement d'ajouter certaines informations.
Au-delà du simple recensement d'artéfacts, il existe de nombreux scénarios d'utilisation
des linkers. On pourrait ainsi imaginer un linker qui se chargerait de télécharger des
fichiers sur un serveur FTP distant en cas de compilation. Ou un autre qui aurait pour
objectif de déplacer les fichiers statiques dans un répertoire donné (.cache.html) et les
fichiers dynamiques (éventuelles pages JSP) dans un autre répertoire.
Autre exemple, dans le cas où une seule permutation est générée (comme pour un
mobile IPhone ou Android), on pourrait imaginer un linker qui optimiserait les permutations
pour embarquer le script de sélection et la permutation au sein du même fichier.
Cela épargnerait une requête HTTP supplémentaire pour charger la permutation.
Bref, le procédé est relativement souple pour permettre tous types de personnalisation.
VII. La pile d'erreurs en production
Java possède un mécanisme nommé la pile d'erreurs ou « StackTrace » qui fournit à
l'utilisateur des données très précieuses sur le contexte d'une exception. Ces informations
contiennent des informations sur la pile d'exécution (quelle méthode a été
appelée avant l'erreur) ou le numéro de ligne incriminé et la nature de l'exception.
Malheureusement, lorsqu'il s'agit d'exception dans le monde JavaScript, les choses
peuvent vite virer au cauchemar tant il existe d'implémentations différentes en fonction
des navigateurs.
Ainsi, Firefox fournit une propriété exception.stack contenant le message, le nom
de fichier et la ligne. Opera intègre dans le message ces trois informations et requiert
une analyse de la chaîne de caractères pour extraire un format exploitable. Quant aux
autres navigateurs, ils ne proposent tout bonnement rien de très évolué dans ce
domaine à part le type arguments et ses propriété arguments.callee et
arguments.caller.callee.
Voici à titre d'exemple un code JavaScript utilisant err.stack :
<html>
<head>
<script type="text/javascript">
function fonc1() {
try {
var i = null;
i.toto();
}
catch(err) {
window.alert(err.message + "\n" + err.stack)
}
}
function fonc2() {
fonc1();
}
</script>
</head>
<a href="#" onClick="fonc1()">Génère exception </a>
</body>
</html>
|
Le résultat sous IE 8, Firefox 3.5, Opera, Safari et Chrome est sans équivoque et
démontre la difficulté d'une gestion commune en JavaScript.

Les piles d'erreurs non uniformes
Le comportement des exceptions en production est aléatoire et surtout peu explicite
(c'est évidemment un doux euphémisme). Pour s'en convaincre voici une copie
d'écran d'une exception générée manuellement.

Une exception générée en mode obfusqué
Vous aurez compris que le mécanisme d'obfuscation est le coupable de ces hiéroglyphes.
En renommant systématiquement les méthodes et variables à des fins d'optimisation,
GWT perd forcément en expressivité. On perd également au passage les
numéros de lignes concernées par la pile.
Tout l'intérêt de GWT va consister à fournir le socle permettant d'unifier (via la
liaison différée) la pile d'appels et la gestion des erreurs. Ce procédé s'appelle
« l'émulation de la pile » et fait partie des nouveautés de GWT 2.
L'activation de cette fonctionnalité s'effectue en positionnant à vrai la propriété
compiler.emulatedStack déjà présente dans le noyau de GWT au travers du module
com.google.gwt.core.EmulateJsStack.
<module>
<inherits name='com.google.gwt.user.theme.standard.Standard'/>
<set-property name="compiler.emulatedStack" value="true" />
(...)
</module>
|
Une fois l'émulation activée, les exceptions deviennent comme par magie beaucoup
plus explicites. Seuls les noms de méthodes restent cryptés. Voici un code Java levant
une exception lors d'un appel natif : voyons le résultat en termes de restitution en
mode production dans un navigateur.
public void onModuleLoad() {
try {
fonc1();
} catch (Exception e) {
Window.alert(printStackTrace(e));
}
}
private String printStackTrace(Throwable e) {
StringBuffer msg = new StringBuffer();
msg.append(e.getClass() + ":" + e.getMessage() + "\n");
for (StackTraceElement se : e.getStackTrace()) {
msg.append("at " + se.getClassName() + "."
+ se.getMethodName() + "(" + se.getFileName() + ":"
+ se.getLineNumber() + ")\n");
}
return msg.toString();
}
public void fonc1() {
fonc2();
}
private native void fonc2()
;
|
L'erreur est générée dans la fonction fonc2() elle-même appelée par fonc1().
Excepté les noms des méthodes, les numéros de lignes et les noms des fichiers source
sont cohérents.

La pile d'erreurs après émulation par GWT
Il est fort probable qu'à l'avenir GWT propose un mécanisme permettant de traduire
une pile d'appels obfusquée via un appel RPC ou une API spécifique. En attendant,
le plus simple est d'activer le mode Pretty avec pour résultat de magnifiques piles.

La pile d'erreurs et le mode Pretty
VII.1. Table des symboles
Nous l'avons vu, le mode Pretty est une des solutions à l'affichage d'une pile d'appels
lisible. En production, il n'est pas toujours possible de l'activer car la taille du fichier
JavaScript aura tendance à grossir exagérément.
Pour répondre à cette problématique, GWT génère un fichier texte contenant les
différents symboles JavaScript générés par le compilateur ainsi que leur correspondance
non cryptée. Ce fichier est généré dans le répertoire paramétré via l'option
–extra. Son nom est celui de la permutation suffixée par .symbolMap.
Il suffit de lire ce fichier pour obtenir le nom lisible d'une méthode donnée ainsi que
sa localisation exacte dans le code source. Voici un exemple de fichier de symboles :
# { 3 }
# { 'user.agent' : 'gecko1_8' }
# jsName, jsniIdent, className, memberName, sourceUri, sourceLine
ow,,boolean[],,Unknown,0
pw,,byte[],,Unknown,0
qw,,char[],,Unknown,0
L,,com.dng.client.AsyncSample,,file:/C:/java/projects/AsyncSample/src/
com/dng/client/AsyncSample.java,14
S,com.dng.client.AsyncSample::$clinit()V,com.dng.client.AsyncSample,$cl
init,file:/C:/java/projects/AsyncSample/src/com/dng/client/
AsyncSample.java,14
T,com.dng.client.AsyncSample::$onModuleLoad(Lcom/dng/client/
AsyncSample;)V,com.dng.client.AsyncSample,$onModuleLoad,file:/C:/java/
projects/AsyncSample/src/com/dng/client/AsyncSample.java,16
U,,com.dng.client.AsyncSample$1,,file:/C:/java/projects/AsyncSample/
src/com/dng/client/AsyncSample.java,18
V,com.dng.client.AsyncSample$1::$clinit()V,com.dng.client.AsyncSample$1
,$clinit,file:/C:/java/projects/AsyncSample/src/com/dng/client/
AsyncSample.java,18
W,,com.dng.client.CRMScreen,,file:/C:/java/projects/AsyncSample/src/
com/dng/client/CRMScreen.java,6
X,com.dng.client.CRMScreen::$clinit()V,com.dng.client.CRMScreen,$clinit
,file:/C:/java/projects/AsyncSample/src/com/dng/client/CRMScreen.java,6
Y,com.dng.client.CRMScreen::$show(Lcom/dng/client/
CRMScreen;)V,com.dng.client.CRMScreen,$show,file:/C:/java/projects/
AsyncSample/src/com/dng/client/CRMScreen.java,7
Z,,com.google.gwt.animation.client.Animation,,jar:file:/C:/gwthack/
trunk/build/dist/gwt-0.0.0/gwt-user.jar!/com/google/gwt/animation/
client/Animation.java,28
cb,com.google.gwt.animation.client.Animation::$$init(Lcom/google/gwt/
animation/client/
Animation;)V,com.google.gwt.animation.client.Animation,$$init,jar:file:
/C:/gwthack/trunk/build/dist/gwt-0.0.0/gwt-ser.jar!/com/google/gwt/
animation/client/Animation.java,28
|
À terme, vous aurez compris que l'idée est de fournir des outils qui pourront
décrypter une pile d'erreurs, et ce, simplement en s'appuyant sur le contenu de la
table des symboles.
VIII. Liens


Les sources présentées sur cette page sont libres de droits
et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation
constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ©
2009 Sami Jaber. Aucune reproduction,
même partielle, ne peut être faite de ce site et de l'ensemble de son contenu :
textes, documents, images, etc. sans l'autorisation expresse de l'auteur.
Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 €
de dommages et intérêts.
Cette page est déposée.