Jérémy Le Piolet - Blog

Gradle, Windows et l’effroyable compilation lente

Petit retour d’expérience sur une compilation Gradle qui prenait des plombes et sur comment j’ai réussi à revenir à des durées acceptables.

TL;DR : j’ai compilé sous Linux.

Le problème

Il y a quelques temps, j’ai prêté main-forte sur le développement d’une application Android de taille conséquente. Pas mal de code réparti dans plusieurs modules, des librairies externes, le tout généré par des builds différents combinés avec plusieurs product flavors : bref, un joli projet.

J’ouvre donc mon Android Studio, je récupère le code depuis le repository et en avant le build. Il s’interrompt régulièrement pour installer telle ou telle version d’outils puis, au bout de 15-20 minutes, fin de build. Je lance alors le déploiement sur mobile, histoire de vérifier que tout fonctionne bien. 8 minutes. Bon, c’est le premier lancement, mise en cache, tout ça, pas grave.

Je commence à regarder le code et toucher quelques éléments. Re-build. 6 minutes. Mince, ça commence à être un peu long, surtout pour un build partiel. Auquel il faut ajouter quelques bonnes minutes de déploiement sur le mobile. Pas loin de 10 minutes pour tester des modifications, ça commence à faire, il ne faut pas se tromper !

Je relance quelques builds depuis Android Studio : je ne tombe jamais sous les 6 minutes. J’essaye en ligne de commande : c’est mieux mais on reste à plus de 4 minutes, presque 5. Beaucoup trop long.

Je contacte les développeurs principaux pour savoir si c’est normal ou s’ils ont une configuration particulière. R.A.S. de leur côté, le build complet avec clean prend entre 50s et 1 minute, le build partiel beaucoup moins.

Ce projet commence mal !

L’exploration des solutions

Face à cette situation, il reste à mettre les mains dans le cambouis et passer en revue les différentes pages, FAQ et stackoverflow qui proposent des solutions. Je regarde également la page officielle qui regroupe bon nombre de solutions.

Solution 1 : mettre à jour la machine

Bien entendu, un meilleur processeur ou plus de ram, ça peut aider à améliorer le build.

Mais ma machine dispose d’un processeur multi-cœur i7, de 16 Go de RAM et d’un SSD, je n’ai pas à me plaindre de ce côté là. S’il faut une ferme de serveurs pour compiler l’application, ce n’est peut-être pas bon signe !

Après retour avec l’équipe, je suis mieux équipé qu’eux : ce ne doit donc pas être un problème de matériel, je passe à la suite.

Solution 2 : utiliser une version de Gradle plus récente

Le projet n’utilise pas la dernière version de Gradle, même si celle utilisée n’est pas si ancienne. Je mets quand même à jour histoire de comparer. Passés les problèmes de compatibilité de plugins entre version de Gradle (Sonar par exemple est plutôt capricieux de ce côté là), je relance le build.

Toujours 6 minutes.

Ce n’est donc pas un problème lié à la version de Gradle. Je reset et je passe à la suite.

Profiling

Avant de poursuivre dans la modification des options de build, je me décide à lancer une analyse complète du build pour voir ce qui prend le plus de temps : inutile d’optimiser ce qui n’a pas besoin de l’être.

Pour cela, il faut ajouter l’option –profile lorsque l’on lance le build Gradle (en ligne de commande ou via studio). Cette commande génère un joli rapport au format HTML qui détaille les tâches exécutées et leurs durées respectives.

gradlew --profile --recompile-scripts --rerun-tasks assembleDebug

Les options –recompile-scripts et –rerun-tasks permettent de s’affranchir d’éventuels caches. Le résultat est le suivant :

Résultat de la compilation gradle sous windows. La compilation prend 4m51 dont 4min 45 sur la partie Task execution
Résultat de la compilation Gradle sous Windows

Ce qui prend le plus de temps c’est la compilation en elle-même. Inutile donc d’ajouter l’option offline au build, je ne suis pas ralenti par le réseau.

Solution 3 : mettre à jour les options de build

Une première possibilité est de modifier le build du debug pour ne conserver que le minimum (ne pas charger toutes les confs de langues, etc.). Seulement je ne suis pas seul sur le projet, ni le développeur principal : si les builds marchent pour les autres, il n’y a pas de raisons que ça ne fonctionne pas pour moi.

Une seconde solution est de créer un build qui m’est propre. Je rajoute donc un build debugJeremy, je ne garde que le minimum syndical et relance le tout. Toujours 4 minutes. Je désactive quelques éléments comme le crashanalytics :

android {
  ...
  buildTypes {
    debugJeremy {
      ext.enableCrashlytics = false
      crunchPngs false
      resConfigs "fr", "xxhdpi"
    }
  ...
}

Quelques pouièmes de millisecondes gagnées.

Là encore, ce n’est pas ça. Et ça ne fait que contourner le problème : la compilation en mode debug standard est toujours aussi longue.

J’ouvre mon fichier gradle.properties et je modifie les valeurs disponibles :

org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=1024m

J’augmente significativement la RAM disponible, histoire d’être tranquille et de voir large (même si l’absence d’erreur mémoire semble indiquer que ça ne serait pas un problème de RAM insuffisante).

Pas d’amélioration.

Je joue un peu avec la compilation parallèle des modules, de même que l’option onDemand. 30 secondes. Victoire. Je lance le déploiement : erreur.

J’essaye d’analyser le déroulement du build : ça ressemble à un gros sac de nœuds où certains modules dépendent d’autres et où des tâches sont lancées en même temps que d’autres alors qu’elles ne devraient pas (le clean en même temps que le build, pas bon).

Le débogage de tout ça ressemble à une expédition en tenue d’Adam (et sans couteau) au fin fond de la forêt amazonienne : je passe mon tour.

La résolution

J’étais à deux doigts de jeter l’éponge et de me résigner à passer la moitié de mes journées à attendre des compilations quand un point de la configuration des autres m’a fait tilt : ils sont sous Mac, je suis sous Windows. Se pourrait-il que ? Pourtant, Java, ça fonctionne bien chez les deux, il n’y a pas de raisons. Et puis, des écarts aussi importants…

Changement d’OS

N’ayant pas de Mac sous la main, je me dis que ce qui s’en rapproche le plus est Linux. Après tout, MacOs n’est ni plus ni moins qu’un système Posix bridé avec de jolis icônes.

Ni une ni deux, je télécharge une iso (j’ai pris un Linux Mint xfce histoire d’avoir un système fonctionnel de suite, les addons virtualbox installés et pas trop gourmands en ressources) et installe une machine virtuelle. Je lui octroie deux cœurs du processeur, 8 Go de Ram et j’installe le système.

Installation d’Android Studio, récupération du code source et lancement du build. Installation des dépendances manquantes puis fin du build : 4 minutes. Par rapport au premier build sous Windows, il y a du mieux. Je relance ma petite commande de profilage :

gradlew --profile --recompile-scripts --rerun-tasks assembleDebug
Résultat de la compilation Gradle sous Linux - temps réduit à 58s
Résultat de la compilation Gradle sous Linux

58 secondes. Je suis sur des temps similaires aux autres développeurs : victoire ! Je relance un build partiel : 10 secondes.

La différence est impressionnante, on a divisé le temps par 4 sur un build complet, c’est hallucinant. Le déploiement sur mobile est lui aussi plus rapide. Et le tout, avec moins de RAM disponible, sans configuration ni optimisation particulière. Assez incroyable je dois avouer.

Bilan

Maintenant que cela fonctionne, vient le temps de faire un petit bilan.

Pourquoi est-ce plus lent sous Windows ?

Je ne suis pas spécialement un extrémiste du système d’exploitation mais des différences aussi importante sur une même machine, qui plus est avec des performances plus importantes sur une machine virtuelle, ça laisse songeur.

Une première piste d’allongement du temps serait l’antivirus. Nul doute qu’il scanne l’apk généré, au moins au moment de l’installation sur le mobile, ce qui expliquerait les différences de temps observées lors du déploiement. Peut-être scanne-t-il également les fichiers de cache intermédiaire lors du build ? N’ayant pas la main sur ce dernier (stratégie d’entreprise, tout ça), je ne peux vérifier son impact.

Une autre piste serait le système de fichier en lui même. Gradle, lorsqu’il lance un build, vérifie les éléments mis à jour ou non pour les recompiler. Windows repose sur un système NTFS qui est vieux et pas vraiment optimisé lorsqu’il faut vérifier les dates de mises à jour. Au contraire d’un ext4, beaucoup plus rapide.

Je n’ai pas eu le temps de vérifier comment Gradle gérait ses builds. Mais j’ai déjà expérimenté des écarts avec Symfony lié à cette problématique ; il y a des chances que Gradle subisse la même chose.

Options de compilation et performance

En explorant le net, on trouve toutes sortes de solutions sur comment optimiser le build Gradle. La technologie évoluant assez vite, les réponses d’il y a deux ans sont souvent hors-sujet ou inutiles et un petit retour ne fait pas de mal (gardez à l’esprit que l’article est écrit début 2018, dans 6 mois le contenu sera peut-être hors-sujet) :

  • Vérifiez ce qui est activé par défaut avec votre version de Gradle. Les versions récentes (> 3.0) activent automatiquement le deamon de même que le cache : inutile donc d’y toucher.
  • Faites un profilage avant de jouer avec les options pour bien identifier les points à optimiser.
  • L’option offline n’est utile que si vous êtes en environnement contraint : réseau lent, proxy lent, etc. Le profilage vous permet de voir si ça coince côté réseau ou pas : si rien n’est à signaler de ce côté là, inutile d’activer l’option.
  • Attention avec les options de build parallèle ou partiel : soyez sûr de vos dépendances et de vos tâches au risque de vous retrouver avec un beau sac de nœuds. De plus, il semble que les versions de Gradle avant 4.0 ont des comportements bizarres dans l’exécution des tâches.
  • Configurez correctement vos builds : évitez les dépendances avec version non fixe (les fameuses malib:2:+), retirez celles qui ne servent plus, créez des builds de dev spécifiques sans chargement de tous les fichiers (inutile de charger 20 langues différentes, par exemple).
  • La compilation en ligne de commande est plus rapide qu’avec Android Studio car ce dernier vérifie plus de choses. Attention toutefois à bien lancer toutes les tâches qui vont bien pour la compilation. Le déploiement peut aussi se faire en ligne de commande si besoin (via abd push).
  • Essayez Linux. 😉

Bref, si jamais vous expérimentez des lenteurs de compilation avec Gradle, que vous êtes sous Windows et que vous n’arrivez pas à optimiser, essayez Linux !