Vuncloud Blog
← Retour aux notes de terrain

CI iOS lent ? Pourquoi xcodebuild traîne sur GitHub Actions

Notes de terrain · Même PR : 6 min un jour, 18 l'autre · séparer cold/warm d'abord · Shadow 14 jours · warm P95 14:12→6:05 ·~9 min de lecture

Écran Mac avec Xcode et terminal, diagnostic CI iOS xcodebuild lent sur GitHub Actions
TL;DR · Voir clair, puis agir
  • Sur 187 PRs, 86,6 % sont des warm — le SLA quotidien doit viser le warm, pas mélanger le cold dans le P95
  • Changement de deps, cache vidé, scheme différent → cold ; un écart 2–3× en temps mur est normal
  • Même en warm : cache miss, jobs qui se battent pour le disque, tests + signature dans le même job → ça continue de fluctuer
  • Ordre grossier : file → séparer cold/warm → cache → concurrence → matériel en dernier (cascade temps)

Données complètes → Pilier · Benchmark

86,6%
échantillons Shadow : warm build
14:12→6:05
warm P95 (macos-latest → M4 dédié)
2–3×
écart typique cold vs warm

1. CI loterie : même commit, temps différent

Si vous cherchez CI iOS lent ? Pourquoi xcodebuild traîne sur GitHub Actions, ces scènes vous diront quelque chose :

  • Même commit, re-run — temps mur 2–3× d’écart
  • P95 rouge sur le dashboard, ressenti équipe : « le merge n’attend pas si longtemps d’habitude »
  • pod install parfois 30 s, parfois « bloqué »
  • Vendredi après-midi, on évite de merger — peur du run lent

Pas toujours un manque de CPU. Sur 14 jours de Shadow avec des équipes, le plus fréquent : chiffres mélangés, environnement instable — cache qui ne tient pas, disque disputé. Changer de puce vient bien après.

Cette note ne traite qu’une chose : pourquoi les builds fluctuent. Job en file ? → article file. Cache, achat vs location → cluster cache et ROI.

2. cold et warm : ne pas tout mélanger

Piège classique : tout verser dans un seul P95. Quelques cold tirent la queue — les données crient « lent tous les jours », le merge quotidien dit autre chose.

Définitions · comparaison Shadow
  • warm : deps inchangées, cache présent, même scheme — compile incrémentale ; représente le « merge normal »
  • cold : lockfile modifié, cache effacé, nouveau target/scheme, premier run sur le runner — resolve, pod install, rebuild large

SLA : warm P50/P95. cold sur sa propre courbe — pas lié à l’expérience merge.

2.1 Quand ça devient cold

Sur macos-latest, le cold est plus fréquent — workspace nettoyé en fin de job, le cache ne « s’installe » pas :

Déclencheur temps ajouté typ. indices log
Podfile.lock modifié +3–8 min pod install, Downloading dependencies
DerivedData miss / effacé +5–15 min CompileSwift, reconstruction .o complète
changement scheme / target +2–10 min autre xcodebuild -scheme
resolve SPM modifié +1–5 min Resolve Package Graph
upgrade mineur Xcode (macos-latest) premier build +10–20 min nouveau SDK / rebuild cache modules

Beaucoup de montées Pod → plus de cold. Pas une machine qui vieillit — un travail de semaine différent. Reporter « semaine Pod » et « semaine dev » séparément.

2.2 Pourquoi le warm reste instable

Même tout en warm, le temps mur peut bouger ~30 %. Souvent :

  • cache miss : clé sans arm64, branche non liée, jobs sur le même slot
  • jobs macOS empilés dans l’org → disque et réseau se ralentissent
  • une fois main seul, une fois tests unit+UI complets — charge différente
  • surface de changement : source Pod vs une ligne SwiftUI — compile très différent
Développeur consultant les logs CI iOS GitHub Actions pour analyser les écarts warm/cold de xcodebuild

3. Où part le temps

Horodater chaque step → cinq blocs de temps mur. Distribution warm (varie selon projet) :

iOS CI wall clock 5 segments (warm · illustrative)
① checkout + env setup        ~0:30 – 1:30
② pod install / SPM resolve   ~0:30 – 2:00   (cold ↑↑)
③ xcodebuild compile+link      ~3:00 – 8:00   (surface de changement)
④ tests (simulator / unit)    ~1:00 – 6:00   (optionnel, souvent sous-estimé)
⑤ archive + codesign           ~1:00 – 4:00   (pipeline release)

warm P50 total typique : 6 – 14 min

En pratique : durées par step ou time sur pod install, xcodebuild build, xcodebuild test. Bloc ② >5 min → cold/cache ; ③ instable → DerivedData/concurrence ; ④ toujours long → sortir les tests ou nightly.

4. Tests & signature : freins cachés

Beaucoup de « xcodebuild lent » cachent tests ou signature dans le même build :

  • cold start simulateur : premier boot sur CI — sans préchauffage, à refaire chaque job
  • UI tests un ordre de grandeur plus lents que unit ; avec compile dans un job → P95 illisible
  • certificats, Keychain, profiles — souvent reconfigurés à chaque job sur runner hébergé
  • archive/IPA = pipeline release, pas validation PR
Conseil rapide · PR et release à part

PR : build + tests légers, warm P95.
TestFlight/release : workflow et métrique séparés. Mélangés → « temps d’attente merge » ne colle jamais.

5. Mesure Shadow 14 jours

Double piste : macos-latest et Mac mini M4 dédié, 187 PRs chacun, Xcode 16.2 / CocoaPods 1.15.2 alignés. Méthode : benchmark pilier.

classe échantillons part macos-latest P95 M4 dédié P95
warm build 162 86,6% 14:12 6:05
cold build 25 13,4% 19:40 11:20
mélangé (erreur fréquente) 187 100% ~16:00+ ~7:30+

cold+warm dans un P95 → expérience quotidienne surévaluée ~15–25 % → « acheter tout de suite ». Mieux : merge = warm, semaines Pod = cold à part.

Sur M4 dédié, warm reste plus rapide que cold — DerivedData/Pods avec un domicile fixe paie. Détails : cluster cache.

6. Ordre : clarifier → cache

Après la file (ou si elle est faible) — ne pas commencer par l’achat :

  1. étiqueter builds : cold vs warm (Podfile.lock, cache hit)
  2. SLA sur warm P95 seulement ; cold en rapport hebdo ou courbe séparée
  3. stratégie cache DerivedData, Pods, SPM — bonnes pratiques cache (en préparation)
  4. ne pas surcharger les jobs macOS : self-hosted 1–2 parallèles ; hébergé, peu de workspace partagé
  5. séparer validation PR et release/signature
  6. cache insuffisant → M4 / M4 Pro — puce & heures ingénieur
workflow snippet · warm / cold tag (illustrative)
- name: Classify build type
  run: |
    if git diff --name-only HEAD~1 | grep -q Podfile.lock; then
      echo "BUILD_TYPE=cold" >> $GITHUB_ENV
    else
      echo "BUILD_TYPE=warm" >> $GITHUB_ENV
    fi

- name: Record wall clock
  run: |
    echo "build_type=${BUILD_TYPE}" >> metrics.csv
    echo "wall_sec=$(( $(date +%s) - START ))" >> metrics.csv

7. FAQ

2–3× sur le même commit — normal ?

Sur macos-latest, oui : un run cache plein, l’autre miss + file. Comparez pod install et logs cache avant de changer de puce.

Comment calculer le P95 ?

Expérience merge : P95 warm, ~30 échantillons en deux semaines. cold à part — mélangé fausse les achats.

pod install toujours lent — faute de CocoaPods ?

Sur runner hébergé : Pods sans lieu fixe, clé cache sans branche/archi. Self-hosted disque fixe : warm souvent <30 s.

Mac mini M4 supprime la variance ?

warm P95 −57 %, σ −40 % — mais sans séparation cold/warm et design cache, les pics restent sur machine dédiée.

File ou build ?

Début de log : Waiting for a runner. La file compte dans le temps mur, pas dans les minutes GitHub. File ≈0 et toujours lent → côté build de cette note → cache.

Pour aller plus loin ?

YAML cache → cluster cache ; achat vs location → ROI et 500 builds/mois.

8. Conclusion

« CI iOS lent » sur GitHub Actions, ce n’est rarement que du CPU. Plus souvent :

  1. cold et warm dans le même P95
  2. job hébergé qui efface le disque → cache perdu
  3. tests, signature et build PR dans un seul job

D’abord un warm P95 propre, puis cache → concurrence → matériel. En 14 jours Shadow, métriques + cache ont suffi pour passer warm P95 de 14:12 à 6:05 — sans décision d’achat précipitée.

Besoin d’un « domicile » fixe pour DerivedData ?

Vuncloud Cloud Mac M4 Pro : disque données 1 To, actions-runner préinstallé. DerivedData / Pods restent sur le disque — moins de « cold à chaque run » comme sur runner hébergé.

Voir les offres Cloud Mac · FAQ branchement CI/CD

Notes de terrain · CI iOS

Voir vite et lent avant de changer de puce

cold/warm séparés · cache d'abord · Shadow dual-run une semaine

Lire le Benchmark pilier
Offre limitée Voir les offres