Un test XCTest qui ne prenait initialement que 4 secondes peut progressivement atteindre 11 secondes sans faire échouer le pipeline. L’équipe ne s’en aperçoit généralement que lorsque la file d’attente des tâches commence à s’allonger sensiblement. La durée totale de la compilation n’est pas adaptée pour localiser ce type de dérive : résolution des dépendances, compilation, démarrage du simulateur et écriture des résultats y sont tous agrégés. Une méthode plus fiable consiste à conserver le fichier .xcresult de chaque tâche de test exécutée sur un Mac cloud NUMACS, à en extraire la durée de chaque cas de test, puis à la comparer à une référence contrôlée.
Définir d’abord ce que mesure le contrôle
Le contrôle doit répondre à la question « ce même test ralentit-il durablement ? », et non « cette tâche a-t-elle pris quelques secondes de plus que la précédente ? ». Il faut commencer par répartir les métriques sur trois niveaux :
| Niveau | Métrique | Utilisation |
|---|---|---|
| Cas de test | Durée d’une exécution | Localiser précisément une régression |
| Suite de tests | Médiane et quantiles élevés | Observer la dérive globale d’un ensemble de tests |
| Tâche CI | Durée totale, du démarrage à la fin du processus | Détecter une anomalie de l’infrastructure ou de la phase de compilation |
Il ne faut pas bloquer une fusion sur la base d’un seul résultat. Les tests courts sont sensibles aux fluctuations d’ordonnancement, tandis que les tests longs peuvent être fortement affectés par le réseau, les animations ou les attentes asynchrones. Une règle pratique consiste à appliquer un « double seuil » : la valeur candidate ne déclenche un nouveau test que si elle dépasse à la fois l’augmentation relative et l’écart absolu définis par rapport à la référence. Par exemple, avec une référence de 2 secondes, une hausse de 20% qui ne représente que 0.1 seconde ne doit pas provoquer d’échec ; avec une référence de 40 secondes, une augmentation de 8 secondes mérite en revanche d’être examinée.
L’objectif d’un contrôle de performances n’est pas d’obtenir exactement la même valeur à chaque exécution, mais d’identifier au plus tôt les ralentissements reproductibles et attribuables.
Stabiliser l’environnement d’exécution et générer le paquet de résultats
Commencez par fixer le chemin de Xcode, le Scheme, la cible d’exécution et la stratégie de parallélisation. Le modèle du simulateur et la version du système doivent être explicitement définis, sans dépendre de « l’appareil actuellement démarré ». Le niveau de concurrence ne doit pas non plus être modifié dynamiquement au cours d’une même tâche.
set -euo pipefail
RESULT_DIR="$PWD/artifacts"
RESULT_BUNDLE="$RESULT_DIR/RegressionTests.xcresult"
mkdir -p "$RESULT_DIR"
rm -rf "$RESULT_BUNDLE"
xcodebuild test \
-workspace Example.xcworkspace \
-scheme ExampleTests \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=18.0' \
-resultBundlePath "$RESULT_BUNDLE" \
-parallel-testing-enabled NO
Les versions de cet exemple ne constituent qu’une partie de l’environnement d’exécution. Dans un projet réel, il faut verrouiller une version validée de Xcode ainsi que le runtime du simulateur. Exécutez d’abord une tâche de préchauffage exclue des statistiques afin de laisser le simulateur démarrer et les ressources de test nécessaires être écrites sur le disque. Les échantillons officiels doivent comprendre au moins trois exécutions. Si le test présente naturellement une forte variance, augmentez le nombre d’exécutions plutôt que d’assouplir les seuils au point de les rendre inutiles.
Extraire la durée des cas de test depuis xcresult
Les versions récentes de Xcode permettent d’obtenir l’arborescence des tests avec xcresulttool. L’interface de commande évoluant avec Xcode, l’analyseur doit être verrouillé avec la version de Xcode utilisée par la CI. Avant toute mise à niveau, sa compatibilité doit être vérifiée à l’aide de paquets de résultats préalablement conservés.
xcrun xcresulttool get test-results tests \
--path artifacts/RegressionTests.xcresult \
--format json > artifacts/tests.json
jq -r '
.. | objects
| select(.nodeType? == "Test Case" and .duration? != null)
| [.name, .duration] | @tsv
' artifacts/tests.json > artifacts/test-durations.tsv
Avant de passer à la comparaison, vérifiez que le fichier TSV n’est pas vide et que le nombre de tests correspond aux attentes. Un fichier vide ne doit pas être interprété comme une « absence de régression » : il indique généralement une modification de l’interface de commande, l’absence d’exécution des tests ou des critères d’analyse devenus incompatibles. L’identifiant d’un test doit inclure le module, la classe et la méthode. Pour les tests paramétrés, il faut également conserver le nom des paramètres afin d’éviter de fusionner à tort des échantillons distincts.
Conserver les données brutes lors de la normalisation
Convertissez toutes les durées en secondes et associez à chaque enregistrement le commit, la version de Xcode, la cible d’exécution et le numéro de tâche. Les fichiers agrégés facilitent les comparaisons, mais le fichier .xcresult d’origine doit rester disponible comme artefact de la tâche. En cas d’anomalie, il fournit également les informations d’échec, le journal des activités et le contexte des pièces jointes.
Remplacer le dernier résultat par une référence robuste
La référence ne doit pas correspondre à la dernière exécution de la branche principale. Un seul démarrage lent peut fausser les évaluations suivantes, tandis qu’une exécution anormalement rapide peut produire de nombreux faux positifs. Une approche plus robuste consiste à collecter plusieurs échantillons récents issus d’exécutions réussies sur la branche principale, puis à calculer la médiane de chaque test tout en enregistrant un quantile élevé ou l’écart absolu médian.
Il est recommandé d’enregistrer les champs suivants pour chaque test :
{
"ExampleTests.testParsing": {
"median_seconds": 3.84,
"absolute_limit_seconds": 1.5,
"relative_limit": 0.25,
"sample_count": 9
}
}
Lorsqu’une branche candidate dépasse pour la première fois les limites, ne réexécutez que le cas de test concerné ou la suite à laquelle il appartient. La tâche ne doit être marquée comme ayant échoué que si la médiane des nouvelles exécutions reste simultanément supérieure à median_seconds + absolute_limit_seconds et à median_seconds × (1 + relative_limit). Les nouveaux tests commencent par une période d’observation : tant que le nombre d’échantillons est insuffisant, leurs résultats sont uniquement signalés et ne bloquent rien.
Éliminer les faux positifs les plus fréquents
Le démarrage à froid du simulateur constitue la principale source de bruit. L’initialisation des données de test, le premier chargement des polices et la création des tables de base de données peuvent également ralentir la première exécution. Ces opérations doivent être traitées par un préchauffage ou isolées dans un setUp clairement défini. Vérifiez ensuite si le test accède au réseau, attend un délai réel, partage les préférences utilisateur par défaut ou réutilise des fichiers laissés par le cas de test précédent.
L’exécution parallèle peut modifier la concurrence sur le CPU, la mémoire et le disque. Pour établir une référence par cas de test, désactivez le parallélisme. Si l’objectif est d’évaluer le débit réel du pipeline, fixez le nombre de workers et incluez cette valeur dans les dimensions de la référence. Les données issues de versions différentes de Xcode, de runtimes système distincts ou de configurations matérielles différentes ne doivent pas être mélangées directement.
En cas de ralentissement soudain, effectuez les vérifications dans cet ordre :
- Le nombre de tests ou la cible d’exécution ont-ils changé ?
- Une attente s’est-elle produite pendant la compilation, l’installation ou le démarrage du simulateur ?
- Le dépassement de délai provient-il d’une interrogation répétée ou d’une temporisation fixe ?
- Les données de test ont-elles augmenté ou n’ont-elles pas été nettoyées à la fin de l’exécution ?
- Plusieurs tâches utilisent-elles simultanément le même répertoire de travail ?
Rendre les modifications de la référence vérifiables
Le fichier de référence doit être placé sous gestion de versions, mais les tâches de test ordinaires ne doivent pouvoir que le lire, jamais l’écraser automatiquement. Lorsqu’un ralentissement est légitime, une tâche indépendante doit générer les différences à partir d’échantillons stables de la branche principale. Les personnes chargées de la revue doivent pouvoir consulter l’ancienne valeur, la nouvelle valeur, le nombre d’échantillons et la raison du changement.
Le rapport final doit regrouper les résultats en trois catégories : « nouvelles régressions », « performances rétablies » et « en observation ». Il doit également indiquer l’écart absolu, l’augmentation relative et le résultat des nouvelles exécutions. Le contrôle peut ainsi empêcher les dégradations de performances cachées sans bloquer le développement à cause d’une fluctuation ponctuelle du simulateur. Au terme de cette mise en place, l’équipe ne dispose pas d’un simple chronomètre fragile, mais d’une référence de durées XCTest reproductible, explicable et vérifiable.
Questions fréquentes
Pourquoi la durée totale de xcodebuild ne suffit-elle pas ?
Elle inclut la résolution des dépendances, la compilation, le démarrage du simulateur et l’écriture des résultats. Il faut extraire les durées individuelles depuis xcresult.
Faut-il choisir un seuil en secondes ou en pourcentage ?
Il vaut mieux combiner les deux. Une régression n’est retenue que si plusieurs exécutions dépassent à la fois l’augmentation absolue et le seuil relatif.
Que faut-il conserver lors d’une mise à jour de la référence ?
Conservez le fichier de référence, le commit, les versions de Xcode et de la destination, le xcresult original et la justification du changement.
Numacs Mac dans le cloud
Externalisez vos tâches de build sur une station de travail physique dédiée
Deux configurations Apple Silicon sont proposées sur des machines physiques dédiées, jamais virtualisées. Louez-les à la journée, à la semaine, au mois ou au trimestre, avec des nœuds à Singapour, Tokyo, Séoul et Hong Kong.