Chez Hokla, notre politique est de créer un code de qualité tout en respectant nos délais de livraison. Cependant, pendant trois sprints consécutifs, nous avons livré des features en retard vis-à-vis des deadlines prévues avec le client. Afin d’améliorer notre flux de livraison, nous nous sommes lancés dans un Kaïzen (une journée concentré sur l’amélioration continue de nos projets issue de la méthodologie Lean) sur notre dernier sprint et celui-ci a rapidement mis en lumière plusieurs causes expliquant nos retards, en particulier le temps d’exécution très long de notre CI.
Optimisation de la stratégie de cache, Ordonnancement des jobs, … Découvrons ensemble ces changements simples mais terriblement efficaces que vous pouvez mettre en place et qui nous permis de diviser notre temps de CI par 2 🏎️.
Attendre un peu mais tout le temps ….
Notre Kaïzen s’est basé sur l’analyse détaillée de 2 tickets réalisés lors du sprint précédent, illustré dans le schéma suivant :
Le premier ticket (17847), était dédié à la création d'un système de mise à jour pour des SOUP*. Le développement a pris environ 8 heures (en bleu) auxquelles s’ajoutent :
- Du temps où l’action d’un autre membre de l’équipe est nécessaire comme les codes reviews (en blanc).
- Du temps utilisé pour faire tourner des tâches automatisées qui prennent approximativement 1h sur l’ensemble du flux (en rouge).
Le deuxième ticket (17856), axé sur la mise en place de logs, souligne encore plus le temps dédié à attendre la CI et la CD. Sur une durée totale de développement de 2 heures et 30 minutes, pas moins de 51 minutes sont consacrées à leur exécution !
Ainsi, le point de blocage identifié est le temps considérable que passent les développeurs à attendre que les tâches automatisées, en particulier la CI et la CD, soient exécutées. Il est alors l’heure de répondre à la question à mille francs : Pourquoi ces étapes sont-elles aussi chronophages ? ⏰
Une CI avec 3 défauts
Nous avons continué notre Kaïzen en détaillant chaque étape de notre CI. Celle-ci est composée de 3 blocs qui se suivent séquentiellement. Le chemin critique est illustré dans le schéma ci-dessous :
En rouge sont indiquées les étapes les plus gourmandes en temps. Grâce à ce schéma, nous avons repéré 3 pistes d’amélioration :
1. Les Tests d'Intégration
Dans notre CI, le premier job englobe divers tests, tels que le linter et les tests unitaires, exécutés en parallèle. Cependant, les tests d'intégration se distinguent par leur durée significative, impactant ainsi le chemin critique de notre CI. La question clé pour atteindre notre objectif est donc la suivante : "Pouvons-nous réduire le temps nécessaire pour exécuter les tests d'intégration ?”
2. L'utilisation du cache
L'utilisation du cache peut théoriquement entraîner des gains de temps considérables. Cependant, nous observons en pratique que notre cache n'est pas exploité entre les différents jobs. Les étapes d'installation sont systématiquement ré-exécutées, entraînant ainsi une perte de temps précieux. Une nouvelle question se pose à nous : “Quelle est la meilleure façon d’utiliser le cache dans notre CI ?”
3. L'ordre d'exécution des Jobs
Le troisième défaut majeur de notre CI concerne l’ordre d'exécution des jobs, avec deux points d'attention spécifiques :
- Le job de coverage s'exécute à chaque cycle de la CI et dépend du job de test.
- Le job SonarQube dépend du résultat du job de coverage.
Nous allons examiner les raisons derrière ces dépendances afin de répondre à la question suivante : "Est-il possible de réorganiser l'ordre d'exécution des jobs afin d'optimiser le temps d'exécution sans compromettre la qualité de notre CI ?”
Le plan d’attaque est prêt, il est temps de passer à l’action ⚔️
Réarranger l’ordre d’exécution des jobs
Notre premier choix a été de remettre en cause l’exécution du job de coverage. En effet, il parait beaucoup plus logique que celui-ci soit seulement exécuté en CD. À partir de cette idée, nous avons donc :
- Totalement supprimé le job de coverage de notre CI.
- Parallélisé le job SonarQube avec le job de test. Son temps d’exécution est maintenant masqué par celui des test d’intégration.
Avant :
Après :
Vous auriez raison de souligner que le job SonarQube dépendait du job de coverage et que cette nouvelle disposition doit donc avoir un impact négatif ! En effet, SonarQube utilise le taux de coverage calculé pour mettre un jour un graphique. Dans la nouvelle configuration, le taux de coverage tombe à 0% lorsqu’une CI est exécutée 😢.
Cependant, nous avons décidé que ce petit inconvénient était négligeable vis à vis du temps gagné. Et oui, 7 minutes de gagnées pour un coût de développement presque nul, nous n’avons pas hésité longtemps.
Continuons l’exploration des solutions, en nous concentrant désormais sur l’optimisation du cache dans une CI.
Optimisation de la stratégie de cache
Il se trouve qu’après notre première modification, nous avons remarqué que nous n’avions plus besoin d’utiliser du cache entre nos différents jobs, puisque ceux-ci s’exécutent en parallèle. Notre optimisation revient donc à supprimer l’étape d’initialisation du cache !
Changement du chemin critique
Nouveau chemin critique :
Avec ces simples changements, nous avons déjà atteint notre objectif de diviser par 2 notre temps d’exécution de CI !
Nous avons décidé de stopper ici les améliorations de notre CI mais nous pourrions aller plus loin en nous plongeant dans l’optimisation des tests d’intégration. Si jamais de votre côté, ce sont vos tests unitaires qui ralentissent votre flux de développement, nous vous recommandons l’article de Clément, qui lui a permis de diviser le temps d’exécution de ses tests unitaire par 10 !
Il reste un dernier point qui nous a permis de gagner de précieuses minutes sur le temps d’exécution de notre CD.
Optimisation de la phase d’installation pour les jobs annexes
Notre job de coverage est maintenant uniquement présent dans la CD. Nous avons remarqué que celui-ci n’avait besoin que de 2 librairies externes pour fonctionner, mais que nous installions toutes les librairies du projet lors de l’initialisation de ce job.
Notre amélioration: utiliser ++code>npx++/code> au lieu de ++code>yarn run++/code> afin de n’installer que les librairies nécessaires.
Résultats: Encore 1 minute de gagnée en CD pour un changement simpliste !
Utiliser ++code>npx++/code> peut être extrêmement utile si vous avez une CI composée de multiple job annexes n’ayant pas besoin de toutes les dépendances de votre projet 👍.
Analyse des résultats
Pour évaluer l’efficacité de nos changements, nous avons comparé le temps de CI et de CD sur 3 tickets avant et après modification.
Notre CI :
Notre CD :
Nous avons réalisé un gain de 8min 30 minutes sur notre CI et 7 min sur notre CD.
Dans le cas de notre équipe (2 développeurs poussant 2 pull requests durant un sprint de 10 jours), ces gains représentent 6 heures de temps de développement ! Le temps parfait pour écrire cet article 😉 !
Conclusion
Plonger dans les détails du processus CI/CD est parfois une tâche que les développeurs délèguent fréquemment aux équipes de DevOps. Cependant, investir du temps pour comprendre en details le flux peut révéler des améliorations simples mais très efficaces, qui, cumulées, peuvent rapidement se traduire par un gain de temps considérable pour toute l'équipe. Dans notre cas, de simple changements dans l'agencement des jobs, la stratégie de mise en cache et l'installation des dépendances ont changés notre quotidien.
Et vous, quelles améliorations évidentes pouvez-vous apporter à votre CI ?