Engineering-Praxis

Build-I/O auf dem Cloud-Mac mit Spotlight- und Dateiwächter-Analyse stabilisieren

Build-I/O auf dem Cloud-Mac mit Spotlight- und Dateiwächter-Analyse stabilisieren

Wird derselbe Commit auf einem Cloud-Mac dreimal hintereinander gebaut, kann der erste Build normal laufen, der zweite plötzlich deutlich länger dauern und der dritte wieder das gewohnte Tempo erreichen. Gleichzeitig ist die CPU nicht dauerhaft ausgelastet und der Netzwerkdownload längst abgeschlossen. Solche Schwankungen werden häufig fälschlich als Ressourcenmangel interpretiert. Tatsächlich kann Spotlight gerade DerivedData durchsuchen, ein Editor den gesamten Workspace rekursiv überwachen oder mehrere CI-Jobs gleichzeitig in dasselbe Cache-Verzeichnis schreiben. Entscheidend ist daher nicht, sofort sämtliche Caches zu leeren, sondern den Istzustand zu sichern, die I/O-Aktivität zu messen und die konkurrierenden Zugriffe schrittweise einzugrenzen.

Zuerst eine vergleichbare Build-Basislinie schaffen

Fixieren Sie vor der Analyse den Commit, die Xcode-Version, das Build-Ziel und die Cache-Pfade. Erfassen Sie den erstmaligen Download von Abhängigkeiten nicht zusammen mit späteren inkrementellen Builds. Während eines Builds sollten außerdem keine Bereinigungsskripte laufen.

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

Protokollieren Sie die verstrichene Gesamtzeit, den maximal belegten residenten Speicher, die Phase der Abhängigkeitsauflösung im Log sowie andere Jobs, die während einer Schwankung aktiv waren. Sind alle drei Durchläufe langsam, sollten meist das Projekt oder die Toolchain geprüft werden. Fallen dagegen nur einzelne Durchläufe auf, spricht das eher für konkurrierende I/O-Aktivität im Hintergrund.

Beobachtung Zuerst prüfen
CPU-Auslastung niedrig, Build bleibt dennoch stehen Warten auf das Dateisystem, Verzeichnissperren
mds oder mdworker aktiv Indizierungsbereich von Spotlight
Normale Laufzeit nach dem Schließen des Editors Rekursive Dateiüberwachung
Problem nur bei parallelen Jobs Gemeinsam genutztes DerivedData oder gemeinsam genutzter Paket-Cache

I/O-Zustand mit Systemwerkzeugen erfassen

Prüfen Sie zuerst den Indizierungsstatus und zeichnen Sie anschließend während eines auffälligen Builds die Dateizugriffe auf. fs_usage benötigt Administratorrechte und erzeugt sehr viele Ausgaben. Begrenzen Sie deshalb zunächst die Prozesse und filtern Sie danach anhand relevanter Pfadbestandteile.

mdutil -s /

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

Prüfen Sie die zugehörigen Prozesse in einem zweiten Terminal:

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

Wenn mdworker nur kurz neu ausgecheckten Quellcode liest, reicht das nicht als Nachweis für die eigentliche Ursache. Aussagekräftiger ist eine anhaltende Prüfung zahlreicher Zwischenprodukte im auffälligen Zeitfenster, wenn sich die gescannten Pfade zugleich mit den Schreibpfaden des Builds überschneiden.

Führen Sie vor der Beweissicherung kein rm -rf DerivedData aus. Das Leeren des Caches kann den nächsten Build zusätzlich verlangsamen und die Hinweise darauf beseitigen, welcher Prozess wiederholt auf welche Dateien zugreift.

Indizierungsgrenzen von Spotlight einschränken

Die Indizierung des System-Volumes sollte nicht pauschal deaktiviert werden. Entwickler sind möglicherweise weiterhin auf die Systemsuche angewiesen, und eine globale Änderung würde die tatsächlich problematische Verzeichnisstruktur verdecken. Robuster ist es, häufig neu erzeugte und jederzeit wiederherstellbare Caches auf ein separates APFS-Volume zu verschieben und nur dieses Volume anzupassen.

Prüfen Sie den Einhängepunkt des Cache-Volumes und führen Sie anschließend Folgendes aus:

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

Quellcode, Dokumentation und andere durchsuchbare Inhalte bleiben weiterhin indiziert. DerivedData, der Download-Cache für Pakete, Testanhänge und temporäre Archivverzeichnisse werden dagegen auf dem separaten Volume abgelegt. Soll die Indizierung später wieder aktiviert werden, verwenden Sie:

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

Signaturmaterial, langfristig aufzubewahrende Artefakte und temporäre Caches dürfen nicht innerhalb derselben Bereinigungsgrenze liegen. Der Ausschluss von der Indizierung verhindert lediglich konkurrierende Scan-Zugriffe und ersetzt weder Zugriffskontrollen noch eine angemessene Datenklassifizierung.

Rekursive Dateiwächter und gemeinsam genutzte Caches steuern

Editoren, Codegeneratoren und Entwicklungsserver überwachen häufig das Stammverzeichnis des Repositorys. Erfassen ihre Regeln auch .git, DerivedData, Testanhänge oder Paket-Caches, kann jeder Build Zehntausende nutzlose Ereignisse auslösen.

Überwachungsbereich verkleinern

Beschränken Sie die überwachten Ziele auf Quellcode- und Konfigurationsverzeichnisse und schließen Sie Folgendes ausdrücklich aus:

Beenden Sie zunächst den verdächtigen Editor oder Agenten und führen Sie danach dieselbe Basislinie erneut aus. Sind die Schwankungen verschwunden, aktivieren Sie die Prozesse einzeln wieder. So lässt sich der Verursacher zuverlässiger bestimmen, als wenn alle Werkzeuge gleichzeitig geändert werden.

Separate Schreibpfade für parallele Jobs verwenden

Serielle Builds können den Projekt-Cache wiederverwenden. Bei parallelem CI dürfen jedoch nicht mehrere Jobs in dasselbe DerivedData schreiben. Der Pfad sollte mindestens eine Kennung für das Repository und den Job enthalten:

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"

Ein Paket-Cache kann für Lesezugriffe gemeinsam genutzt werden, doch beim Aktualisieren von Abhängigkeiten kann weiterhin Schreibkonkurrenz entstehen. In Umgebungen mit hoher Parallelität lässt sich die Abhängigkeitsauflösung zunächst in einem kontrollierten Schritt ausführen. Die Build-Jobs verwenden anschließend das stabile Auflösungsergebnis.

Bereinigung und Regressionsprüfung trennen

Ein Bereinigungsskript darf nicht gleichzeitig mit xcodebuild laufen. Löschen Sie abgelaufene Caches anhand der Job-Verzeichnisse, statt das oberste Verzeichnis täglich bedingungslos zu leeren. Lassen Sie zunächst nur die Kandidaten ausgeben:

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

Nachdem Verzeichnistiefe und Aufbewahrungsdauer geprüft wurden, sollte der eigentliche Löschvorgang in einen separaten Job verschoben werden. Führen Sie zur Abnahme die festgelegte Basislinie erneut dreimal aus und beobachten Sie dabei gleichzeitig fs_usage. Ein erfolgreiches Ergebnis ist nicht ein einzelner besonders schneller Build. Entscheidend ist, dass Indizierungsprozesse die Build-Verzeichnisse nicht mehr dauerhaft durchsuchen, parallele Jobs keine gemeinsamen Schreibpfade verwenden und die Zeitverteilung der Build-Phasen über mehrere Durchläufe hinweg stabil bleibt.

Dokumentieren Sie abschließend die Xcode-Version, den Commit-Hash, den DerivedData-Pfad, den Pfad des Paket-Caches, die aktiven Überwachungsprozesse und den Indizierungsstatus in den Build-Diagnosedaten. Treten später erneut Schwankungen auf, können zuerst die Unterschiede der Umgebungen verglichen werden, statt wieder mit einer Cache-Bereinigung und neuen Vermutungen zu beginnen.

Häufig gestellte Fragen

Sollte Spotlight auf dem gesamten Systemvolume deaktiviert werden?

Nicht als Standardmaßnahme. Prüfen Sie zuerst, ob mds oder mdworker dauerhaft auf Build-Verzeichnisse zugreift. Ist der Konflikt bestätigt, ändern Sie nur die Indizierung eines separaten Cache-Volumes.

Dürfen parallele CI-Jobs dasselbe DerivedData verwenden?

Serielle Builds desselben Projekts können es wiederverwenden. Parallele Jobs sollten getrennte Pfade je Repository, Branch oder Job-ID erhalten, damit gleichzeitige Schreibzugriffe und Sperrkonflikte vermieden werden.

Exklusiver physischer Mac mini

Wählen Sie einen Cloud-Mac für Ihre nächste Entwicklungs-Pipeline

Vergleichen Sie drei feste Konfigurationen an vier asiatischen Standorten und wählen Sie die Mietdauer passend zu Ihrer tatsächlichen Arbeitslast. Jede Miete umfasst einen exklusiven physischen Rechner, keine virtuelle Maschine.

Konfiguration auswählen und mieten