blog.vfg.lol

18 minutes pour vérifier 3 lignes de code : l'enquête sur notre CI

Un test qui passe en 150 secondes à lui seul, 45 fichiers qui démarrent chacun leur serveur, et une PR de texte qui déclenche la batterie complète. On a mesuré, et on sait enfin où part le temps.

TL;DR

Livrer le lot S1 de Jah — 96 lignes, dont 40 de code — a demandé 18 minutes de vérification automatique. Le coupable n'est pas le lint (10 secondes) ni les tests unitaires (2 min 30) : c'est l'E2E, à 76 % du temps. Trois causes mesurées, dont une ligne à changer pour gagner 6 minutes : passer les tests de navigateur de 2 à 4 en parallèle. On a aussi trouvé un test qui coûte 150 secondes à lui seul et une règle qui fait payer 20 minutes à une PR de documentation.

Le point de départ : une intuition fausse

Quand une CI est lente, on accuse le plus visible : le lint, l'installation des dépendances, les tests unitaires. On regarde le log, on voit passer des centaines de lignes, et on se dit « c'est forcément là ».

C'est faux. Voici le vrai découpage d'un job complet, mesuré au step près sur la PR #124 :

ÉtapeDuréePart du total
Récupérer le code + préparer Node40 s4 %
Lint (909 fichiers)9,8 s1 %
Tests unitaires + intégration (772 cas)152,6 s17 %
Tests E2E (navigateur)665,6 s76 %
Audit des dépendances0,5 s—
Total14,6 min

Une seule étape fait les trois quarts du travail. Tout le reste est déjà rapide.

La règle de trois minutes

Avant de chercher plus loin, un mot sur ce qu'est un test E2E, parce que le nom ne dit pas grand-chose.

Un test unitaire vérifie une fonction : « le calcul d'étapes donne 4 ». Il prend un millième de seconde.

Un test d'intégration vérifie plusieurs morceaux ensemble, sans navigateur : « deux comptes se parlent, le message arrive en base ». Un dixième de seconde.

Un test E2E — end to end, « du début à la fin » — vérifie le parcours complet d'une vraie personne : il ouvre un vrai navigateur, charge la vraie page, clique sur le vrai bouton, et regarde ce qui s'affiche. C'est le plus précieux (il prouve que ça marche pour de vrai) et le plus cher : il faut démarrer un serveur, créer une base, lancer un Chromium, puis attendre que tout se mette en place.

Chez Jah, 45 fichiers font ça. Et chacun démarre son propre serveur, sa propre base et son propre navigateur.

Cause nº 1 : 45 démarrages, 2 à la fois

Le projet lance les fichiers E2E deux par deux (--test-concurrency=2). C'est prudent : ça évite que deux serveurs se disputent un port.

Sauf que la machine du runner GitHub a 4 cœurs. On en utilise donc la moitié, et il faut environ 23 vagues pour passer les 45 fichiers. Chaque vague paie le coût fixe : démarrer un serveur, migrer une base, ouvrir un navigateur.

La correction tient en un caractère : --test-concurrency de 2 à 4. Les ports sont déjà dynamiques ; la mesure de vitesse d'images (fps) tourne déjà seule, dans sa propre passe, pour ne pas être faussée. On l'a vérifié : la concurrence 2 a été choisie par prudence, pas par contrainte technique.

Gain mesuré par le calcul : 5 à 6 minutes par vérification. C'est le meilleur rapport effort/rendement de tout le dépôt — une ligne pour un tiers du problème.

Cause nº 2 : un fichier qui pèse un cinquième

On a mesuré les 45 fichiers un par un. Le classement est brutal :

Fichier E2EDuréeCas
reparation-r4.test.mjs150,1 s19
reparation-r3a.test.mjs76,8 s6
reparation-r3b.test.mjs74,0 s11
phase6-vraie-vie.test.mjs55,9 s7
phase1-core.test.mjs54,4 s11
les 40 autres~700 s à elles toutes—

Le premier fichier, à lui seul, fait 21 % de tout l'E2E. Il s'appelle reparation-r4.test.mjs : 515 lignes, 19 cas, dont trois attentes à durée fixe (waitForTimeout, la béquille qu'on utilise quand on ne sait pas quel signal attendre).

Trois choses se cumulent ici :

  1. Il est trop gros. 19 cas dans un seul fichier, c'est 19 cas qui attendent la fin des 18 autres avant que le navigateur se ferme.
  2. Il attend des durées fixes là où il pourrait attendre un état : « attends 500 ms » au lieu de « attends que le texte apparaisse ».
  3. Il mélange deux natures. Il contient des cas de style (taille du texte, contraste, lisibilité) qui n'ont pas besoin d'un navigateur pour être vérifiés — un test unitaire sur la feuille de style suffirait, et prendrait un millième du temps.

La correction : le couper en deux fichiers, sortir les cas de style vers des tests unitaires, remplacer les attentes fixes par des attentes d'état. Gain : 1 à 2 minutes, et un fichier qui redevient lisible.

Cause nº 3 : la PR de texte qui paie 20 minutes

C'est le plus irritant, et le plus instructif.

La PR #123 ne contenait aucun code. C'était de la documentation : corriger des compteurs dans un tableau de bord, séparer deux portes dans une fiche, réécrire une règle dans les prompts. Vérification : 19,6 minutes.

Pourquoi ? Parce que le projet a un plan intelligent : « si un diff ne touche que de la documentation, on ne lance que le lint (40 secondes) ; sinon, toute la batterie ». Ce plan fonctionne. Mais il a une exception : dès qu'un fichier CHANGELOG est modifié, la batterie complète est déclenchée.

Or — et c'est la blague — toute PR est censée mettre à jour le journal des changements. La conséquence est mécanique : l'exception est devenue la règle. Toute PR paie 15 minutes de tests, même quand elle ne change pas une ligne de code exécutable.

Trois façons de régler ça :

OptionEffetCe qu'on perd
Un CHANGELOG seul ne déclenche plus le complet−15 min sur ces PRrien : le journal ne change pas le comportement
Une PR ouverte en brouillon ne paie que le lint−15 min sur les poussées intermédiairesrien : le complet revient au passage « prête à relire »
Ne pas relancer la CI sur main après une fusion quand l'arbre est identique−10 à −15 min par fusionrien : c'est le même code, déjà vérifié

La troisième mérite un mot. Aujourd'hui, quand on fusionne une PR verte dans main, la même batterie repart de zéro. On vérifie deux fois exactement le même arbre. Le plan de septembre prévoyait déjà de l'éviter ; ça n'a jamais été appliqué.

Ce qu'on croyait, et ce qu'on a trouvé

Un mot sur une intuition qu'on a eue et qu'il faut jeter : « la CI est lente parce que la file d'attente des machines GitHub est saturée ».

C'est faux, et la mesure est sans appel : l'attente avant qu'un job démarre est de 0,2 minute. Le job commence tout de suite. Les 18 minutes sont du travail réel, pas de l'attente.

C'est important, parce que ça change la conclusion : il ne sert à rien de réduire le nombre de runs (ce qu'on croyait), il faut réduire ce que chaque run fait.

Le vrai enseignement

L'équipe avait déjà fait un audit de sa CI en septembre, et il était bon. Il avait identifié que les E2E prenaient 70 % du temps, et il proposait des corrections justes. Pourquoi n'a-t-il pas suffi ?

Parce qu'il mesurait la durée des suites, et pas leur nombre.

Un test ne ralentit pas tout seul. Une CI grossit parce qu'on en ajoute — et chaque nouveau lot livré en ajoute au moins un. En septembre, l'E2E prenait 166 secondes. Aujourd'hui : 665 secondes. Quatre fois plus. Personne n'a rien ralenti : on a simplement ajouté des tests, 45 fois, sans jamais regarder le total.

Il manquait donc un plafond. Pas une règle écrite dans un document — une mesure affichée qui alerte :

Durée de la vérification complète : 18 min. Fichiers E2E : 45. Au-delà de 8 minutes, on optimise avant de livrer un nouveau lot.

Une règle écrite dans un fichier se perd. Une mesure affichée dans le tableau de bord se voit tous les jours. C'est exactement la leçon du linter de cette même journée : une règle outillée tient, une règle écrite ne tient pas.

Ce qu'on fait, dans l'ordre

PrioritéActionGainEffort
1Concurrence E2E : 2 → 4−5 à −6 minune ligne
2Couper reparation-r4 (150 s) et sortir ses cas de style−1,5 min30 min
3CHANGELOG seul ≠ batterie complète−15 min sur ces PR15 min
4Ne pas revérifier main après une fusion identique−10 à −15 min1 h
5Regrouper les fichiers E2E par domaine−1 à −2 min1–2 h

Les deux premières se font tout de suite : une ligne et un déplacement de fichier, pour 7 minutes gagnées par vérification, soit 40 % de la CI. Le reste suivra.

Et pour Jah, concrètement ?

Rien de visible pour toi : ce sont des outils de travail, pas des fonctionnalités. Mais il y a un effet que tu verras.

Chaque nouvelle idée livrée — une fenêtre qu'on déplace, un réglage de texte, une couleur de maison — doit être vérifiée avant d'arriver chez toi. Plus cette vérification est lente, moins on livre de choses, et plus longtemps tu attends. Réduire la CI de 18 à 7 minutes, c'est livrer deux fois plus souvent sans jamais baisser la garde.

C'est aussi une question de vérité : une vérification qui prend un quart d'heure, on est tenté de la sauter « juste cette fois ». Une vérification qui prend trois minutes, on la lance sans y penser.


Mesures prises le 30 septembre 2026 entre 07 h 30 et 07 h 40, sur les 30 derniers runs GitHub et sur deux jobs complets (PR #124 et release alpha.12). Le rapport détaillé, avec les commandes utilisées, est dans docs/plans/ci-tests.md du dépôt Jah Messenger.

source markdown

Concepts de cet article

garde

Partage le plus de concepts avec