Programmation GWT 2 : Sous le capot de GWT

| Sami Jaber | Ricky81 |

Cet article est un extrait gratuit de l'ouvrage Programmation GWT 2 - Développer des applications RIA et Ajax avec GWT publié aux éditions Eyrolles.
Il s'agit du chapitre 12 intitulé "Sous le capot de GWT".

Commentez

- Titre : Programmation GWT 2 : Sous le capot de GWT

- Auteurs : Sami Jaber, Ricky81

- Parution : 10 décembre 2009

- Mise à jour : 10 décembre 2009

- Licence : 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.

Sommaire

- 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() {
// En fonction de l'animal, aboie, miaule, etc.
};
}

// Et la méthode onModuleLoad()
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 \x{0096}draftCompile et \x{0096}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){ // (2)
$Object(this$static); // (3)
$$init_0();
this$static.race = race;
this$static , age;
return this$static;
}

function $getRace(this$static){ // (5)
return this$static.race;
}

function $parle(){} // (4)

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); // (1)
$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();
(...)
// Remplace tous les GWT.create() et génère le code JavaScript
ResolveRebinds.exec(jprogram, rebindAnswers);

// Optimise un peu (plus rapide) ou beaucoup (forcément plus lent)
if (options.isDraftCompile()) {
draftOptimize(jprogram);
} else {
// Presse le citron de l'optimisation jusqu'à ne plus pouvoir gagner d'octet
do {
boolean didChange = false;

// Enlève tous les types non référencés, champs, méthodes, variables&#8230;
didChange = Pruner.exec(jprogram, true) || didChange;
// Rend tout "final", les paramètres, les classes, les champs, les méthodes
didChange = Finalizer.exec(jprogram) || didChange;
// Réécrit les appels non polymorphiques en appels statiques
didChange = MakeCallsStatic.exec(jprogram) || didChange;

// Supprime les types abstraits
// - Champs, en fonction de leur utilisation, change le type des champs
// - Paramètres : comme les champs
// - Paramètres de retour de méthode : comme les champs
// - Les appels de méthodes polymorphiques : dépend de l'implémentation
// - Optimise les conversions et les instanceof
didChange = TypeTightener.exec(jprogram) || didChange;

// Supprime les types abstraits lors des appels de méthode
didChange = MethodCallTightener.exec(jprogram) || didChange;

// Supprime les éventuelles portions de code mort
didChange = DeadCodeElimination.exec(jprogram) || didChange;

if (isAggressivelyOptimize) {
// On supprime les fonctions en déplaçant leur corps dans l'appelant
didChange = MethodInliner.exec(jprogram) || didChange;
}
} while (didChange);
}

(...)

// Generate a JavaScript code DOM from the Java type declarations
(...)
JavaToJavaScriptMap map = GenerateJavaScriptAST.exec(jprogram, jsProgram,
options.getOutput(), symbolTable);

// Réalise cette fois des optimisations JavaScript
if (options.isAggressivelyOptimize()) {
boolean didChange;
do {
didChange = false;
// Supprime des fonctions JavaScript inutilisées, possible
didChange = JsStaticEval.exec(jsProgram) || didChange;
// Inline JavaScript function invocations
didChange = JsInliner.exec(jsProgram) || didChange;
// Remove unused functions, possible
didChange = JsUnusedFunctionRemover.exec(jsProgram) || didChange;
} while (didChange);
}

// Permet de créer une pile d'appels pour simuler la StackTrace Java
JsStackEmulator.exec(jsProgram, propertyOracles);

// Casse le programme en plusieurs fragments JavaScript
// (aka CodeSplitting)
SoycArtifact dependencies = splitIntoFragment(logger, permutationId,
jprogram, jsProgram, options, map);

// Réalise l'opération d'obfuscation (OBFS, PRETTY, DETAILED
Map<JsName, String> obfuscateMap = Maps.create();
switch (options.getOutput()) {
case OBFUSCATED: (...)
case PRETTY: (...)
case DETAILED: (...)
}

// Génère le rendu final au format texte.
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() {
// En fonction de l'animal, aboie, miaule, etc.
};
}

// Classe onModuleLoad()
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 :

// Code Java
Animal instance = new Animal();
instance.parle()
// Est transformé en JavaScript par
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()
// Devient via le jeu de l'inlining
Forme f = new Carre(2);
int a = f.length * f.length;
// Puis
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);
}
}

// La compilation génère la trace suivante avec l'option activée

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.

Les options du compilateur

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é.

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 \x{0096}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.

Options système (certaines sont peu documentées)

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.

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 :

1. 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.

2. 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.

3. 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).

4. 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() { }
// Remplacé par la méthode d'initialisation suivante
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 à :

1. Créer une classe dérivant de com.google.gwt.core.ext.Linker.

2. 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.

3. Définir et ajouter le linker personnalisé dans le fichier de configuration du module (<module>.gwt.xml).

4. 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="";
// Récupère la liste de tous les artéfacts
ArtifactSet toReturn = new ArtifactSet(artifacts);
for (Artifact artifact : toReturn) {
// Et trie seulement les fichiers générés
if (artifact instanceof EmittedArtifact) {
EmittedArtifact fic = (EmittedArtifact) artifact;
// Stocke dans une chaîne de caractères le nom du fichier généré
artifactList = fic.getPartialPath() + "," + new
Date(fic.getLastModified()).toString() + "\n" + artifactList ;
}
}
// Ajoute à la liste précédente un nouveau fichier recensant
// les artéfacts
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 a lieu ici
func.toto();
}-*/;

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 \x{0096}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

- Consultez la table des matières complète ainsi que l'avant propos du livre

- L'ouvrage complet sur Amazon.fr

- Discussion autour de l'ouvrage "Programmation GWT 2 - Développer des applications RIA et Ajax avec GWT"

- Le forum d'entraide francophone sur GWT