Pratiques d’ingénierie

Stabiliser les E/S de build sur Mac cloud avec Spotlight et les observateurs de fichiers

Stabiliser les E/S de build sur Mac cloud avec Spotlight et les observateurs de fichiers

Un même commit est compilé trois fois de suite sur un Mac cloud : le premier build se déroule normalement, le deuxième ralentit soudainement, puis le troisième retrouve ses performances habituelles. Le CPU ne reste pas saturé et les téléchargements réseau sont terminés depuis longtemps. Ce type de variation est souvent attribué à tort à un manque de ressources. En réalité, Spotlight peut être en train d’analyser DerivedData, un éditeur peut surveiller récursivement tout l’espace de travail, ou plusieurs tâches CI peuvent écrire simultanément dans le même répertoire de cache. La bonne approche ne consiste pas à vider immédiatement tous les caches, mais à préserver l’état du système, à quantifier les E/S, puis à isoler chaque source de contention.

Établir une base de référence comparable pour les builds

Avant toute investigation, fixez le commit, la version de Xcode, la cible de build et les chemins de cache. Ne mélangez pas le premier téléchargement des dépendances avec les builds incrémentaux suivants dans un même jeu de mesures, et n’exécutez pas de script de nettoyage pendant un build.

set -o pipefail

WORKSPACE="$HOME/ci/work/app"
DERIVED="$HOME/ci/derived/app-main"
PACKAGES="$HOME/ci/packages/app"

mkdir -p "$DERIVED" "$PACKAGES"
cd "$WORKSPACE"

xcodebuild \
  -resolvePackageDependencies \
  -clonedSourcePackagesDirPath "$PACKAGES"

for run in 1 2 3; do
  /usr/bin/time -lp xcodebuild \
    -workspace App.xcworkspace \
    -scheme App \
    -configuration Debug \
    -destination 'generic/platform=iOS Simulator' \
    -derivedDataPath "$DERIVED" \
    -clonedSourcePackagesDirPath "$PACKAGES" \
    build 2>&1 | tee "$HOME/ci/build-$run.log"
done

Consignez le temps écoulé, la mémoire résidente maximale, la phase de résolution des dépendances visible dans les journaux, ainsi que la présence éventuelle d’autres tâches au moment du ralentissement. Si les trois exécutions sont lentes, examinez en priorité le projet ou la chaîne d’outils. Si seules certaines exécutions sont anormales, une contention d’E/S en arrière-plan est plus probable.

Observation Vérification prioritaire
CPU peu sollicité, mais build à l’arrêt Attentes du système de fichiers, verrous de répertoires
mds ou mdworker actif Périmètre d’indexation de Spotlight
Retour à la normale après la fermeture de l’éditeur Surveillance récursive des fichiers
Problème uniquement avec des tâches parallèles DerivedData ou cache de paquets partagé

Capturer l’activité d’E/S avec les outils système

Commencez par vérifier l’état de l’indexation, puis capturez les accès aux fichiers pendant un build anormal. fs_usage nécessite des privilèges d’administration et produit un volume de données important : limitez d’abord les processus observés, puis filtrez la sortie à l’aide de mots-clés correspondant aux répertoires.

mdutil -s /

sudo fs_usage -w -f filesys mds mdworker_shared |
  grep -E 'DerivedData|SourcePackages|/ci/work/'

Dans un autre terminal, examinez les processus concernés :

ps -axo pid,ppid,%cpu,%mem,etime,command |
  grep -E 'mds|mdworker|xcodebuild|swift-frontend|SourceKit' |
  grep -v grep

Si mdworker ne fait que lire brièvement du code source récemment récupéré, cela ne suffit pas à établir qu’il est à l’origine du problème. Un indice plus probant est une analyse prolongée d’un grand nombre d’artefacts intermédiaires pendant la période anormale, avec des chemins d’analyse qui recoupent ceux dans lesquels le build écrit.

N’exécutez pas rm -rf DerivedData avant d’avoir recueilli les éléments de diagnostic. Vider le cache peut ralentir le build suivant et supprimer les indices permettant d’identifier « quel processus accède de façon répétée à quels fichiers ».

Restreindre le périmètre d’indexation de Spotlight

Il est déconseillé de désactiver directement l’indexation du volume système. Les développeurs peuvent avoir besoin de la recherche système, et une modification globale masquerait également le véritable problème d’organisation des répertoires. Une solution plus sûre consiste à placer les caches fréquemment régénérés et entièrement reconstructibles sur un volume APFS distinct, puis à ne modifier que les paramètres de ce volume.

Après avoir vérifié le point de montage du volume de cache, exécutez :

mdutil -s /Volumes/CICache
sudo mdutil -i off /Volumes/CICache
mdutil -s /Volumes/CICache

Conservez l’indexation du code source, de la documentation et des ressources qui doivent rester consultables. Placez sur ce volume DerivedData, le cache de téléchargement des paquets, les pièces jointes de test et les répertoires temporaires d’archives. Pour réactiver ultérieurement l’indexation :

sudo mdutil -i on /Volumes/CICache
sudo mdutil -E /Volumes/CICache

Ne placez pas les éléments de signature, les artefacts à long terme et les caches temporaires dans un même périmètre de nettoyage. L’exclusion de l’indexation ne traite que la contention liée à l’analyse ; elle ne remplace ni le contrôle des autorisations ni la classification des données.

Maîtriser la surveillance récursive et les caches partagés

Les éditeurs, générateurs de code et serveurs de développement surveillent souvent la racine du dépôt. Si les règles de surveillance incluent .git, DerivedData, les pièces jointes de test ou les caches de paquets, chaque build peut déclencher des dizaines de milliers d’événements inutiles.

Réduire le périmètre de surveillance

Limitez la surveillance aux répertoires du code source et de la configuration, et excluez explicitement les éléments suivants :

Fermez d’abord l’éditeur ou l’agent suspect, puis exécutez de nouveau la même base de référence. Si les variations disparaissent, réactivez les processus un par un : il sera ainsi plus facile d’identifier le responsable que si vous modifiez tous les outils simultanément.

Attribuer des chemins d’écriture distincts aux tâches parallèles

Les builds séquentiels peuvent réutiliser le cache du projet. En revanche, plusieurs tâches CI parallèles ne doivent pas écrire dans le même répertoire DerivedData. Le chemin doit contenir au minimum les identifiants du dépôt et de la tâche :

SAFE_REPO="${REPO_NAME//[^a-zA-Z0-9_-]/_}"
SAFE_JOB="${JOB_ID//[^a-zA-Z0-9_-]/_}"

export DERIVED_DATA="$HOME/ci/derived/$SAFE_REPO/$SAFE_JOB"
export PACKAGE_CACHE="$HOME/ci/packages/$SAFE_REPO"

mkdir -p "$DERIVED_DATA" "$PACKAGE_CACHE"

Le cache de paquets peut être partagé en lecture, mais la mise à jour des dépendances peut toujours provoquer des écritures concurrentes. Dans les environnements à forte concurrence, effectuez d’abord la résolution des dépendances lors d’une étape contrôlée, puis faites utiliser son résultat stable aux tâches de build.

Séparer le nettoyage de la validation de non-régression

Les scripts de nettoyage doivent éviter toute exécution de xcodebuild en cours et supprimer les caches expirés par répertoire de tâche, plutôt que de vider systématiquement le répertoire racine chaque jour. Commencez par afficher uniquement les éléments candidats :

DERIVED_ROOT="$HOME/ci/derived"

if ! pgrep -x xcodebuild >/dev/null; then
  find "$DERIVED_ROOT" \
    -mindepth 2 -maxdepth 2 \
    -type d -mtime +7 -print
fi

Après avoir validé la profondeur des répertoires et la durée de conservation, placez la suppression dans une tâche distincte. Pour la validation, exécutez de nouveau trois fois la base de référence fixe tout en surveillant fs_usage. Le critère de réussite n’est pas un build ponctuellement très rapide : les processus d’indexation ne doivent plus analyser continuellement les répertoires de build, les tâches parallèles ne doivent partager aucun chemin d’écriture et la répartition des phases doit rester stable d’un build à l’autre.

Enfin, consignez dans le diagnostic du build la version de Xcode, le commit, le chemin de DerivedData, le chemin du cache de paquets, les processus de surveillance actifs et l’état de l’indexation. Lors de la prochaine variation, vous pourrez commencer par comparer les différences d’environnement au lieu de repartir d’hypothèses après avoir vidé les caches.

Questions fréquentes

Faut-il désactiver Spotlight sur tout le disque système ?

Non, pas par défaut. Vérifiez d’abord si mds ou mdworker accède durablement aux répertoires de build, puis limitez l’exclusion à un volume de cache dédié afin de conserver la recherche normale du système.

Plusieurs tâches CI peuvent-elles partager le même DerivedData ?

Le partage convient surtout aux builds séquentiels d’un même projet. Pour des tâches parallèles, utilisez un chemin distinct par dépôt, branche ou identifiant de tâche afin d’éviter les écritures concurrentes et les invalidations imprévisibles.

Mac mini physique dédié

Choisissez un Mac dans le cloud pour votre prochain pipeline de développement

Comparez trois configurations fixes et quatre nœuds en Asie, puis choisissez la durée de location adaptée à votre charge de travail. Chaque location correspond à une machine physique dédiée, et non à une machine virtuelle.

Choisir une configuration et louer