Optimisation d’un build Grunt

Je travaille sur la version française d'une application web américaine. L'équipe aux Etats-Unis produit la plupart du code et mon équipe en France modifie l’existant et ajoute des fonctionnalités pour le marché Français. L'application est en Angular 1 et 2/4. Le système du build mis en place aux Etats-Unis utilise Grunt. Le problème rencontré par mon équipe était que le build nominal pour les développeurs prend 30 secondes aux Etats-Unis et 3m30s en France. Aux Etats-Unis, tout le monde travaille sur OSX. En France nous sommes obligés d’utiliser Windows.

La première étape d'une optimisation

Comme vous le savez, la première règle de l'optimisation est : avant d'optimiser, il faut mesurer. Pour ce faire j'ai utilisé un outil qui s'appelle time-grunt. Il faut l'installer avec npm :

[racine de mon projet]$ npm install time-grunt

L'utilisation est très simple. En haut du gruntfile, il faut ajouter :

module.exports = function(grunt) {
 require('time-grunt')(grunt);  // ajouter cette ligne
…

Lorsque je lance le build, le résumé apparaît, par tâche, à la fin :

$ grunt development --skipAll
Execution Time (2017-01-13 10:28:30 UTC+1)
loading tasks                 8.8s ██████ 4%
clean:ts                     24.4s ████████████████ 11%
shell:buildTS                19.3s ████████████ 9%
clean:build                  11.2s ███████ 5%
less:production              18.8s ████████████ 9%
copy:development           1m 6.2s █████████████████████████████████████████ 31%
stripJsonComments:default       6s ████ 3%
jsonlint:default              5.6s ████ 3%
angularFileLoader:index        47s █████████████████████████████ 22%
Total 3m 32.5s

Sans avoir fait beaucoup de modifications, nous avons une bonne vision de l'endroit où notre build passe du temps. J’ai lancé le build plusieurs fois et les temps restaient stables. J’étais donc prêt à passer à la 2e étape.

Où optimiser ?

La 2e règle de l'optimisation est : optimiser d'abord ce qui prend le plus de temps. Cela parait évident mais j'ai souvent vu des développeurs passer du temps sur une optimisation uniquement parce qu'ils avaient une très bonne idée de la façon d'optimiser. Si nous regardons les chronos donnés par time-grunt, l'étape la plus chronophage est le copy:files (66s). Après un peu d'analyse, je trouve que l’étape fait comme son nom l'indique : il copie les fichiers du répertoire source vers le répertoire de build.

66s pour copier les fichiers paraît démesuré, d'autant que le même build sur un mac prend 30 secondes et il fait tout. Quand je regarde les logs je vois que l'étape doit copier environ 4600 fichiers et 1400 répertoires. Windows n'a jamais eu le système de fichiers le plus rapide mais il n'est pas possible qu'une machine Windows en 2017 prenne 60s pour copier 4600 fichiers. Néanmoins, avant de rentrer dans le javascript de la tâche grunt, je fais une petite expérience : je fais la même copie mais en ligne de commande DOS :

xcopy app\* build\ /e /i

Avec mon chronomètre j’arrive à environ 55s. Autrement dit, cette copie banale de Windows prend presque deux fois plus de temps que tout le build sur OSX. Je ne sais pas si c'est le système de fichier, l'anti-virus sur la machine, le chiffrement du disque ou autre chose qui rende la copie si pâteuse mais je ne perds pas de temps à investiguer. Même s'il s'avère que le problème vient de l'anti-virus, par exemple, je ne peux pas le désactiver. Je dois chercher un autre contournement.

Quand je rentre dans les détails de la tâche, je vois qu'elle est assez bête. Au lieu de copier uniquement les fichiers qui ont changé, la tâche fait une copie de tout le projet. Sur un mac, cette « bêtise » ne pose pas de problème, mais sur Windows nous devons chercher une meilleure façon de le faire.

C'est ici que l'on voit l'importance d'un bon écosystème lorsque nous développons. J'ai déjà utilisé l'outil time-grunt pour mon analyse de build. Après quelques recherches, je trouve grunt-sync, un outil qui synchronise deux répertoires. Je serai toujours obligé de copier tous les fichiers après un « grunt clean » par exemple. Mais dans le cycle de développement, c’est uniquement le fichier qui a changé que je veux copier. Alors, si je peux copier 1 fichier au lieu de 3000, je devrais gagner de temps.
J’installe le package :

$ npm install grunt-sync

Ensuite je regarde comment il fonctionne. Il y a une source, une cible et quelques options. Dans le Gruntfile, je configure deux répertoires cibles. Il y a une option qui synchronise dans les deux sens mais j’ai des étapes Grunt après la copie qui modifie les fichiers dans le répertoire de build. Je ne veux pas ces changements dans le code source. Je laisse la synchronisation juste dans un sens :

grunt.config('sync', {
 main: {
   files: [{
     src: [ 
       '*.html',
       'app/**',
       '!app/**/css/*.less',
       '!app/**/*-spec.js',
       'tmp/**/**'
     ],
     dest: config.build.dir
   },
   {
     cwd: 'node_modules/',
     src: [
       config.app.wildcards.vendorJSinits,
       config.app.wildcards.vendorFONTS,
       config.app.wildcards.runtimeLess,
       config.app.wildcards.vendorJsSrc,
       config.app.wildcards.vendorCssSrc
     ],
     dest: config.build.vendorDir
   }]
}});

Maintenant, j'enlève ma tâche copy, je réorganise le build légèrement et j'ajoute la nouvelle tâche « sync ». Je teste. Après un clean du répertoire build, la tâche sync (qui remplace la tâche copy) prend 46s. C’est mieux. Si je modifie 1 fichier et relance le build, la tâche prend 20s.

Pour le cycle « normal » de développement, j'ai gagné 45 secondes : de 65s à 20s ! Et, plus important, j'ai une réduction de 20% sur le temps total de build. Le build reste trop lent mais je suis satisfait de mon intervention.

Les prochaines étapes :
2m45s (3m30 - 45s), cela reste trop lent. Et la prochaine réduction ne sera pas aussi simple. Mais je pense pouvoir faire mieux. Affaire à suivre...

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *