Il y a 10 ans -
Temps de lecture 7 minutes
Java, JavaScript ou les deux ?
Une des grandes tendances du moment est le multi-langage. Cet article ne va pas déroger à la règle en prenant un exemple concret implémenté en Java et en proposant une version en JavaScript. Nous étudierons ensuite les avantages et inconvénients des deux solutions et regarderons un peu ce que donnent les performances comparées.
L’exemple de base : rechercher le nombre d’occurences d’un mot dans un document et en déduire un score en fonction d’un second document
Présenté comme ça, ce n’est peut-être pas très clair mais en fait c’est limpide. Imaginez que vous ayez à gérer un calendrier de sessions pour une conférence bien connue comme Devoxx par exemple. Une manière de stocker ou d’exposer ces informations serait de fournir un fichier JSON. Pour les besoins spécifiques à notre application nous l’avons quelque peu remanié (à l’aide de ce coffeescript) afin qu’il ressemble à ce flux. Voici la structure du fichier :
{"days":[ { "day":"2012-11-12", "slots":[ { "slot":"09:30", "talks":[ { "id":759, "title":"Android Development Code Lab", "uri":"http://cfp.devoxx.com/rest/v1/events/presentations/2098", "summary":"Dive into some of the latest and greatest features of the Android operating system in this interactive code lab. Developers will need Eclipse or Intellij installed as well as the Android SDK and the latest version of Android Development Tools and APIs.", "room":"BOF 1", "speaker":"Nick Butcher", "day":"2012-11-12", "from":"09:30", "to":"12:30", "type":"Hands-on Labs", "speakers":["Nick Butcher", "Richard Hyndman"], "tags":["SDK", "android", "mobile"] }, (...) ] }, (...) ] }, (...) ]}
On y voit donc une liste de days comprenant des slots eux-même comprenant des talks.
Voici ensuite un second fichier – beaucoup plus simple – qui donne le score de chaque talk identifié par son id. :
{ "759":1, "948":5, "866":3, (...) }
Le traitement que l’on va développer en Javascript va remplacer la classe Java Scorer
qui permet de calculer le score d’un mot-clé. Pour chaque talk qui contient ce mot-clé, on somme le score de chaque talk. Vous pouvez aller regarder l’implémentation de cette classe.
Implémentation en JavaScript
Nous allons utiliser underscore.js pour écrire un code concis et lisible.
Tout d’abord, on déclare les données dont on a besoin :
var planning = JSON.parse(injectedPlanning); var starsPerTalk = JSON.parse(injectedStarsPerTalk); var talksContent = _.chain(planning.days) .pluck("slots").flatten() .pluck("talks").flatten() .map(function (talk) { return { id:talk.id, words:new String().concat( talk.title, " ", talk.summary, " ", talk.speaker, " ", talk.speakers.join(" "), " ", talk.tags.join(" ") ) }; }) .value();
Si on imagine que injectedPlanning
et injectedStarsPerTalk
contiennent les deux flux évoqués plus haut sous la forme de chaînes de caractères, alors on les parse en JSON. talksContent
crée une liste d’objets avec deux propriétés :
id
: l’identifiant du talkwords
: tous les mots extraits de certaines des propriétés d’un talk
À noter notamment dans cette transformation l’utilisation de la fonction _.pluck() qui extrait d’une liste la propriété spécifiée. Il s’agit sans doute de l’usage le plus commun de _.map(). Voici l’implémentation de cette fonction :
_.pluck = function(obj, key) { return _.map(obj, function(value){ return value[key]; }); };
Avec ces données en entrée, on peut maintenant écrire la fonction qui détermine le score en fonction d’un mot-clé :
function computeScore(keyword) { var searchedKeyword = new RegExp(keyword, "im"); var talkIdsWithKeyword = _.reduce(talksContent, function (talkIds, talk) { if (searchedKeyword.test(talk.words)) { talkIds.push(talk.id); } return talkIds; }, []); var score = _.chain(starsPerTalk) .filter(function (stars, talkId) { return _.contains(talkIdsWithKeyword, Number(talkId)); }) .reduce(function (totalScore, score) { return totalScore + score; }, 0) .value(); return Number(score); }
Cette fonction extrait tout d’abord les talks qui contient le mot-clé. Cette liste est ensuite utilisée pour sommer les scores.
Intégration dans le projet java
Maintenant que notre traitement est écrit en JavaScript, on aimerait le faire compiler et le faire exécuter par la JVM. Voici la marche à suivre.
Tout d’abord obtenir une instance de ScriptEngine
:
ScriptEngineManager scriptEngineManager = new ScriptEngineManager(); ScriptEngine scriptEngine = scriptEngineManager.getEngineByName("JavaScript");
Ensuite charger les données et les fichiers JavaScript et les compiler :
scriptEngine.put("injectedPlanning", Resources.toString(talksURL, Charsets.UTF_8)); scriptEngine.put("injectedStarsPerTalk", Resources.toString(votesURL, Charsets.UTF_8)); ((Compilable) scriptEngine).compile(Resources.toString(getResource("underscore-min.js"), Charsets.UTF_8)).eval(); ((Compilable) scriptEngine).compile(Resources.toString(getResource("scorer.js"), Charsets.UTF_8)).eval();
talksURL
et votesURL
sont injectés par Guice. Le fichier underscore-min.js
a été téléchargé et placé dans un répertoire de resources (src/main/resources
par défaut avec Maven). De même pour scorer.js
décrit plus haut.
Enfin ce script engine sera disponible dans la classe sous la forme d’une variable d’instance de type javax.script.Invocable
:
engine = (Invocable) scriptEngine;
Voici la méthode qui évalue le score :
protected int get(String keyword) throws NoSuchMethodException, ScriptException { return ((Double) engine.invokeFunction("computeScore", keyword)).intValue(); }
Un petit test – enfin
Ce n’est pas dans l’état de l’art de tester tardivement mais mieux vaut tard que jamais… Voici donc un petit test pour s’assurer que les deux implémentations renvoient bien le même résultat sur un mot-clé identique :
@Test public void should_get_same_score_as_java_implementation() throws Exception { Scorer javaScorer = new Scorer(new Talks(getResource("planning.json")), new Votes(getResource("starsPerTalk.json"))); JavascriptScorer javaScriptScorer = new JavascriptScorer(getResource("planning.json"), getResource("starsPerTalk.json")); assertThat(javaScorer.get("AngularJs")).isEqualTo(37); assertThat(javaScriptScorer.get("AngularJs")).isEqualTo(37); }
Et bien sûr : c’est vert !
Performances comparées
Pour savoir laquelle des deux implémenations est la plus efficace, voici une petite classe qui affiche un graphique :
import com.google.inject.Guice; import com.google.inject.Injector; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.chart.*; import javafx.stage.Stage; import javax.script.ScriptException; public class PerformanceGraph extends Application { private static final String KEYWORD = "java"; private Scorer javaScorer; private JavascriptScorer javascriptCompiledScorer; public PerformanceGraph() { final Injector injector = Guice.createInjector(new JerseyModule()); javaScorer = injector.getInstance(Scorer.class); javascriptCompiledScorer = injector.getInstance(JavascriptScorer.class); } @Override public void start(Stage stage) throws Exception { stage.setTitle("Java vs Javascript implementation"); final CategoryAxis xAxis = new CategoryAxis(); final NumberAxis yAxis = new NumberAxis(); final AreaChart<String, Number> chart = new AreaChart<>(xAxis, yAxis); chart.setTitle("Java vs Javascript implementation chart"); yAxis.setLabel("nanoseconds"); final XYChart.Series javaPerformance = new XYChart.Series(); javaPerformance.setName("Java"); final XYChart.Series javascriptPerformance = new XYChart.Series(); javascriptPerformance.setName("JavaScript"); for (int i = 0; i < 100; i++) { Long[] performances = performance(); String index = Integer.toString(i); javaPerformance.getData().add(new XYChart.Data<String, Number>(index, performances[0])); javascriptPerformance.getData().add(new XYChart.Data<String, Number>(index,performances[1])); } final Scene scene = new Scene(chart, 800, 600); chart.getData().addAll(javaPerformance, javascriptPerformance); stage.setScene(scene); stage.show(); } private Long[] performance() throws ScriptException, NoSuchMethodException { long now; now = System.nanoTime(); javaScorer.get(KEYWORD); Long java = System.nanoTime() - now; now = System.nanoTime(); javascriptCompiledScorer.get(KEYWORD); Long javascript = System.nanoTime() - now; return new Long[]{java, javascript}; } public static void main(String... args) { launch(args); } }

D’après ce graphique, on peut constater que l’implémentation en JavaScript est deux fois plus rapide qu’en Java.
Plus généralement, ce petit exercice permet d’ouvrir quelques perspectives sur les langages de script de la JVM et la manière de les invoquer. De nombreux langages de scripts sont compatibles avec la JSR-223 à l’origine de l’API comme par exemple Groovy.
Pour l’instant, l’implémentation utilisée dans Java est Rhino développé par Mozilla pour Java 6. Une nouvelle implémentation du langage va voir le jour et a adopté le doux nom de Nashorn. Il promet plus de performances et une meilleure inter-opérabilité entre les mondes Java et JavaScript.
Dans le cas de JavaScript pour cet exemple, on peut même aller plus loin et se demander si on ne pourrait pas déléguer l’exécution de l’évaluation d’un score directement sur le navigateur – déportant ainsi une grande partie du traitement sur les clients.
Commentaire
5 réponses pour " Java, JavaScript ou les deux ? "
Published by Thomas Broyer , Il y a 10 ans
La comparaison serait plus juste si les algos étaient légèrement optimisés: pourquoi faire une Multimap en Java plutôt que concaténer comme en JS ? Pourquoi ne pas faire le toLowerCase au moment du parsing ? et surtout pourquoi faire le toLowerCase sur le keyword à chaque itération plutôt qu’une bonne fois pour tout au début de la méthode (comme la construction de la RegExp en JS) ?
D’ailleurs pourquoi utiliser une RegExp en JS ? (surtout sans vérifier ou échapper l’input, donc en supposant que le mot-clé ne contient aucun caractère « spécial »)
Published by Sébastian Le Merdy , Il y a 10 ans
Toutes ces remarques sont judicieuses. En fait on voulait à tout prix faire gagner JavaScript sur les performances donc on a écrit un code Java pas du tout optimisé :)
Je re-posterai un commentaire avec les performances comparées incluant toutes les améliorations dont tu parles (et peut-être d’autres).
Published by Alexis Kinsella , Il y a 10 ans
Sujet trollesque par excellence! Je vous propose de reprendre le protocole de Sébastian avec vos optimisations par langage et de publier dans les commentaires vos résultats ;) N’hésitez pas à coller des liens de Gists accompagnés des screenshot de résultats ;)
Mon opinion est la suivante: Etant donné que le javascript s’exécute sur la JVM, je ne vois donc pas pourquoi l’implémentation java serait plus lente. J’imagine, qui plus est, que l’adaptation du Runtime Javascript sur la JVM n’est pas aussi optimisé qu’un moteur JavaScript type V8 (En attendant la future hypotétique implémentation du nouveau moteur JavaScript dans la JVM ).
Published by Fabrice Daugan , Il y a 10 ans
Sujet utile pour déporter des règles métier ou des algorithmes Javascript vers son serveur.
La partie concernant les performances porte à confusion : pas de V8, pas de code Javascript optimisé, utilisation de fonctions d’underscore pratiques certes mais séquences d’exécution optimisable, etc. et pourtant 2x plus rapide que la version Java?
D’un autre côté, comme le souligne Thomas, le code Java laisse à désirer. En regardant le code de Votes, on se rend compte que l’instance du parser Gson n’est pas mise en cache.
Donc impossible de faire une idée sur la réalité perte d’efficacité lorsque que l’on veut écrire son code en Javascript pour le faire exécuter sur une JVM.
Plus sérieusement, pour les bench pure JS vs Java vs Rhino : http://www.pankaj-k.net/spubs/articles/beanshell_rhino_and_java_perf_comparison/index.html
Published by ahdat maghribia , Il y a 10 ans
java langage de programmation et le javascript un langage de script