Engineering-Notizen

Eine Laufzeit-Schranke für XCTest mit xcresult aufbauen

Eine Laufzeit-Schranke für XCTest mit xcresult aufbauen

Eine Laufzeit-Schranke für XCTest mit xcresult aufbauen

Ein XCTest, das ursprünglich nur 4 Sekunden benötigte, dauert schleichend 11 Sekunden, während die Pipeline weiterhin vollständig grün bleibt. Meist bemerkt das Team die Veränderung erst, wenn sich die Warteschlange deutlich verlängert. Die gesamte Build-Dauer eignet sich nicht dazu, solche Regressionen einzugrenzen: Abhängigkeitsauflösung, Kompilierung, Simulatorstart und das Schreiben der Ergebnisse fließen alle in diesen Wert ein. Zuverlässiger ist es, bei jedem Testjob auf einem Cloud-Mac von NUMACS die .xcresult-Datei aufzubewahren, die Laufzeiten einzelner Testfälle zu extrahieren und sie mit einer kontrollierten Baseline zu vergleichen.

Zuerst das Messsignal der Schranke definieren

Die Schranke soll beantworten, ob „derselbe Test dauerhaft langsamer wird“, und nicht, ob „dieser Job einige Sekunden länger als der vorherige gedauert hat“. Dafür werden die Metriken zunächst in drei Ebenen unterteilt:

Ebene Metrik Zweck
Testfall Dauer einer einzelnen Ausführung Konkrete Regression lokalisieren
Testsuite Median und hohe Perzentile Gesamtdrift einer Testgruppe beobachten
CI-Job Gesamtdauer vom Start bis zum Ende Auffälligkeiten in der Infrastruktur oder Kompilierungsphase erkennen

Ein einzelnes Ergebnis sollte einen Merge nicht unmittelbar blockieren. Kurze Tests reagieren empfindlich auf Schwankungen bei der Jobplanung; lange Tests können dagegen durch Netzwerkzugriffe, Animationen oder asynchrones Warten ausgebremst werden. In der Praxis bewährt sich ein „doppelter Schwellenwert“: Erst wenn ein Kandidatenwert sowohl den relativen als auch den absoluten Grenzwert gegenüber der Baseline überschreitet, wird ein Wiederholungslauf ausgelöst. Liegt die Baseline beispielsweise bei 2 Sekunden, sollte eine Zunahme um 20 %, die lediglich 0.1 Sekunden ausmacht, nicht zum Fehlschlag führen. Bei einer Baseline von 40 Sekunden ist eine Zunahme um 8 Sekunden dagegen eine Untersuchung wert.

Das Ziel einer Performance-Schranke besteht nicht darin, bei jedem Lauf identische Zahlen zu erhalten, sondern reproduzierbare und eindeutig zuzuordnende Verlangsamungen frühzeitig zu erkennen.

Ausführungsumgebung fixieren und Ergebnispaket erzeugen

Zunächst müssen Xcode-Pfad, Scheme, Ausführungsziel und Parallelisierungsstrategie festgelegt werden. Simulatormodell und Systemversion sind explizit anzugeben; ein „derzeit gestartetes Gerät“ darf nicht als implizites Ziel dienen. Auch die Anzahl paralleler Ausführungen sollte innerhalb desselben Jobs nicht dynamisch geändert werden.

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

Die Beispielversion ist nur ein Bestandteil der Ausführungsumgebung. In einem realen Projekt sollten die freigegebene Xcode-Version und die Simulator-Runtime festgeschrieben werden. Zuerst wird ein Aufwärmlauf ausgeführt, der nicht in die Statistik eingeht. So kann der Simulator vollständig starten und benötigte Testressourcen können auf den Datenträger geschrieben werden. Für die eigentlichen Messungen sind mindestens drei Durchläufe erforderlich. Weist der Test selbst eine hohe Varianz auf, sollte die Anzahl der Durchläufe erhöht werden, anstatt die Schwellenwerte so weit zu lockern, dass sie ihre Aussagekraft verlieren.

Laufzeiten einzelner Testfälle aus xcresult extrahieren

Neuere Xcode-Versionen können den Testbaum über xcresulttool ausgeben. Da sich die Befehlsschnittstelle mit den Xcode-Versionen weiterentwickelt, muss der Parser gemeinsam mit der in CI verwendeten Xcode-Version fixiert werden. Vor einem Upgrade ist seine Kompatibilität anhand gespeicherter Ergebnispakete zu prüfen.

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

Vor dem Vergleich muss geprüft werden, ob die TSV-Datei Daten enthält und ob die Anzahl der Tests den Erwartungen entspricht. Eine leere Datei darf nicht als „keine Regression“ durchgewunken werden. Sie deutet üblicherweise darauf hin, dass sich die Befehlsschnittstelle geändert hat, die Tests nicht ausgeführt wurden oder die Parserbedingung nicht mehr passt. Testkennungen sollten Modul, Klasse und Methode umfassen. Bei parametrisierten Tests muss außerdem der Parametername erhalten bleiben, damit unterschiedliche Datensätze nicht versehentlich zusammengeführt werden.

Bei der Normalisierung die Rohdaten als Beleg erhalten

Alle Laufzeiten werden einheitlich in Sekunden umgerechnet. Jeder Datensatz wird außerdem um Commit, Xcode-Version, Ausführungsziel und Jobnummer ergänzt. Aggregierte Dateien erleichtern den Vergleich, die ursprüngliche .xcresult-Datei sollte jedoch weiterhin als Job-Artefakt gespeichert werden. Bei Auffälligkeiten liefert sie zusätzlich Fehlermeldungen, Aktivitätsprotokolle und den Kontext von Anhängen.

Eine robuste Baseline statt des letzten Ergebnisses verwenden

Die Baseline sollte nicht dem letzten Lauf des Hauptbranches entsprechen. Ein einzelner langsamer Start würde nachfolgende Bewertungen verfälschen, während ein einmalig ungewöhnlich schneller Lauf zahlreiche Fehlalarme erzeugen könnte. Robuster ist es, mehrere der letzten erfolgreichen Stichproben des Hauptbranches zu sammeln, für jeden Test den Median zu bilden und zusätzlich ein hohes Perzentil oder die mediane absolute Abweichung zu speichern.

Für jeden Test sollten folgende Felder gespeichert werden:

{
  "ExampleTests.testParsing": {
    "median_seconds": 3.84,
    "absolute_limit_seconds": 1.5,
    "relative_limit": 0.25,
    "sample_count": 9
  }
}

Überschreitet ein Kandidatenbranch erstmals die Grenzwerte, werden nur die betroffenen Testfälle oder die zugehörige Suite erneut ausgeführt. Erst wenn der Median der Wiederholungsläufe weiterhin sowohl median_seconds + absolute_limit_seconds als auch median_seconds × (1 + relative_limit) überschreitet, wird der Job als fehlgeschlagen markiert. Neue Tests beginnen mit einer Beobachtungsphase. Solange nicht genügend Stichproben vorliegen, werden ihre Ergebnisse nur gemeldet und blockieren keine Änderungen.

Die häufigsten Scheinregressionen ausschließen

Ein Kaltstart des Simulators ist die wichtigste Rauschquelle. Auch die Initialisierung von Testdaten, das erstmalige Laden von Schriftarten und das Anlegen von Datenbanktabellen können den ersten Durchlauf verlangsamen. Diese Arbeiten sollten durch einen Aufwärmlauf oder eine klar abgegrenzte setUp-Phase vom eigentlichen Messwert getrennt werden. Anschließend ist zu prüfen, ob Tests auf das Netzwerk zugreifen, reale Zeitspannen abwarten, gemeinsame Benutzervorgaben verwenden oder Dateien wiederverwenden, die ein vorheriger Testfall hinterlassen hat.

Parallele Tests können die Konkurrenz um CPU, Arbeitsspeicher und Datenträger verändern. Soll eine Baseline für einzelne Testfälle entstehen, ist die Parallelisierung zu deaktivieren. Soll dagegen der reale Durchsatz der Pipeline bewertet werden, muss die Anzahl der Worker festgelegt und als eigene Dimension in der Baseline erfasst werden. Daten aus unterschiedlichen Xcode-Versionen, System-Runtimes oder Hardwarekonfigurationen dürfen nicht direkt zusammengeführt werden.

Bei einer plötzlichen Verlangsamung sollten diese Punkte der Reihe nach geprüft werden:

  1. Haben sich die Anzahl der Tests oder das Ausführungsziel geändert?
  2. Sind Wartezeiten durch Kompilierung, Installation oder Simulatorstart entstanden?
  3. Wird das Timeout durch Polling oder feste Wartezeiten verursacht?
  4. Wurden Test-Fixtures vergrößert oder nach Abschluss nicht bereinigt?
  5. Greifen mehrere Jobs gleichzeitig auf dasselbe Arbeitsverzeichnis zu?

Änderungen an der Baseline überprüfbar machen

Die Baseline-Datei sollte versionsverwaltet werden. Reguläre Testjobs dürfen sie jedoch nur lesen und nicht automatisch überschreiben. Ist eine Verlangsamung sachlich gerechtfertigt, erzeugt ein separater Job auf Grundlage stabiler Stichproben des Hauptbranches einen Änderungsvorschlag. Die Reviewer müssen dabei den alten Wert, den neuen Wert, die Stichprobenzahl und den Grund der Änderung sehen können.

Der Abschlussbericht gruppiert die Ergebnisse in „neue Regressionen“, „behoben“ und „unter Beobachtung“. Zusätzlich führt er die absolute Zunahme, die relative Zunahme und das Ergebnis der Wiederholungsläufe auf. So verhindert die Schranke versteckte Performance-Verschlechterungen, ohne den Entwicklungsprozess wegen einer einzelnen Simulatorschwankung aufzuhalten. Am Ende verfügt das Team nicht über eine fragile Stoppuhr, sondern über eine reproduzierbare, erklärbare und überprüfbare Baseline für XCTest-Laufzeiten.

Häufig gestellte Fragen

Warum eignet sich die gesamte xcodebuild-Laufzeit nicht als XCTest-Grenzwert?

Sie enthält zusätzlich Abhängigkeitsauflösung, Kompilierung, Simulatorstart und das Schreiben der Ergebnisse. Für eine Diagnose müssen die Laufzeiten einzelner Tests aus xcresult ausgewertet werden.

Sollte der Grenzwert absolut oder prozentual definiert werden?

Beide Bedingungen sollten kombiniert werden. Eine Regression gilt erst dann als relevant, wenn absoluter Zuwachs und prozentuale Verschlechterung nach Wiederholungen überschritten bleiben.

Welche Daten gehören zu einer aktualisierten Baseline?

Baseline-Datei, Commit, Xcode-Version, Laufziel, ursprüngliches xcresult-Bundle und Begründung der Änderung sollten gemeinsam archiviert werden.

NUMACS Cloud-Mac

Build-Aufgaben auf eine dedizierte physische Workstation auslagern

Beide Apple-Silicon-Konfigurationen laufen auf dedizierten physischen Maschinen statt in virtuellen Umgebungen. Sie können tage-, wochen-, monats- oder quartalsweise gemietet werden und sind an den Standorten Singapur, Tokio, Seoul und Hongkong verfügbar.

Gerät auswählen und bestellen