TECHNIK-ARTIKEL

Eine Regression-Schranke für iOS-Scrolling mit XCTMetric aufbauen

Eine Regression-Schranke für iOS-Scrolling mit XCTMetric aufbauen

Nachdem eine Produktliste einen zusätzlichen Farbverlauf, Bilddecodierung oder komplexe Schatten erhalten hat, bleiben die Funktionstests weiterhin grün – beim Scrollen kommt es jedoch plötzlich zu Bildaussetzern. Durch einige manuelle Wischbewegungen lässt sich kaum feststellen, ob es sich um gelegentliches Ruckeln oder eine Code-Regression handelt. Zuverlässiger ist es, Simulator, Datensatz und Gesten auf einem cloudbasierten Mac fest vorzugeben, mit XCTMetric wiederholt Laufzeit, CPU-Auslastung und Speicherverbrauch zu erfassen und die kontinuierliche Integration nur bei Abweichungen oberhalb der Baseline zu blockieren.

Zuerst alle relevanten Variablen festlegen

Der erste Schritt einer Performance-Schranke besteht nicht darin, Assertions zu schreiben, sondern Umgebungsrauschen zu reduzieren. Legen Sie die Versionen von Xcode und macOS sowie Simulatormodell, Systemsprache, Displayausrichtung und Testdaten fest. Das Testkonto sollte direkt zur Zielseite führen, damit Anmeldeanfragen, Bilddownloads und Serverantwortzeiten nicht in die Scroll-Messwerte einfließen.

Fügen Sie der Anwendung einen Startparameter hinzu, der nur bei UI-Tests aktiviert wird, beispielsweise -UITestSeed fixed. Damit sollten lokale Fixtures geladen, Listeneinträge in fester Anzahl und Reihenfolge erzeugt und zufällige Animationen deaktiviert werden. Die Liste benötigt außerdem stabile Bedienungshilfen-IDs wie feed.list und feed.reset; der Testcode darf Elemente nicht anhand lokalisierter Texte suchen.

Beim ersten Durchlauf fallen üblicherweise zusätzliche Arbeiten wie die App-Installation, das Laden dynamischer Bibliotheken, die Initialisierung von Schriftarten und das Aufwärmen von Caches an. Führen Sie deshalb vor der eigentlichen Messung einen nicht gewerteten Testlauf aus. Löschen Sie jedoch nicht die Caches, um eine künstliche „reine Kaltstart“-Umgebung zu erzeugen, die nicht der alltäglichen Nutzung entspricht.

Eine Performance-Baseline beschreibt eine vollständige Umgebung und keinen universellen Millisekundenwert, der sich auf jeden Mac und jeden Simulator übertragen lässt.

Einen reproduzierbaren Scroll-Ablauf definieren

Ein einzelner Aufruf von swipeUp() hängt zu stark von der Ausgangsposition und der Listenlänge ab. Ein zuverlässigerer Test kehrt zunächst zum Anfang der Liste zurück, prüft, ob der erste Anker sichtbar ist, und führt anschließend eine festgelegte Anzahl von Gesten aus. Nach jeder Messrunde muss die Ausgangsposition erneut hergestellt werden, damit die nächste Runde nicht am Ende der Liste beginnt.

import XCTest

final class FeedScrollPerformanceTests: XCTestCase {
    func testFeedScrollPerformance() {
        let app = XCUIApplication()
        app.launchArguments += ["-UITestSeed", "fixed"]
        app.launch()

        let list = app.collectionViews["feed.list"]
        XCTAssertTrue(list.waitForExistence(timeout: 10))

        app.buttons["feed.reset"].tap()
        XCTAssertTrue(app.cells["feed.item.0"].waitForExistence(timeout: 5))

        let options = XCTMeasureOptions()
        options.iterationCount = 5

        measure(
            metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()],
            options: options
        ) {
            app.buttons["feed.reset"].tap()
            for _ in 0..<6 {
                list.swipeUp(velocity: .fast)
            }
        }
    }
}

Die Schaltfläche zum Zurücksetzen kann ausschließlich im Testmodus eingeblendet werden, muss jedoch einen deterministischen Scroll-Vorgang ausführen, statt die Daten erneut abzurufen. Verwendet die Seite paginiertes Laden, sollte die Netzwerkschicht durch lokale Antworten ersetzt oder die Dauer des Nachladens ausdrücklich in einem separaten Test gemessen werden.

Auf einem festen Ziel ausführen und Ergebnisse archivieren

Listen Sie zunächst die verfügbaren Simulatoren auf, um sicherzustellen, dass die kontinuierliche Integration nicht unbemerkt auf ein anderes Gerät gewechselt hat. Geben Sie anschließend einen eindeutigen Pfad für das Ergebnis-Bundle an. Ein vorhandenes Bundle muss vorher gelöscht werden, da xcodebuild das Überschreiben verweigert.

set -euo pipefail

RESULT="$PWD/artifacts/FeedScroll.xcresult"
rm -rf "$RESULT"
mkdir -p "$PWD/artifacts"

xcodebuild test \
  -workspace App.xcworkspace \
  -scheme AppUITests \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
  -only-testing:AppUITests/FeedScrollPerformanceTests \
  -resultBundlePath "$RESULT"

OS=latest eignet sich für Pipelines, die stets der aktuellen Toolchain folgen. Für langfristige Vergleiche sollte hingegen eine konkrete Runtime festgelegt und die Baseline nach einem Upgrade von Xcode oder des Betriebssystems neu erstellt werden. Bei Jobs auf OVPS-Knoten sollten ebenfalls Hostkonfiguration, Commit-ID, Xcode-Version und Simulator-Runtime protokolliert werden, damit ein Umgebungs-Upgrade nicht fälschlicherweise als Codeverschlechterung eingestuft wird.

Lesen Sie vor der Verwendung des in der aktuellen Xcode-Version enthaltenen xcresulttool die Hilfe, da sich Unterbefehle zwischen Toolchains ändern können. Unabhängig davon, ob ein Skript oder ein Parser für Testberichte verwendet wird, sollte die ursprüngliche xcresult-Datei als Artefakt eines fehlgeschlagenen Laufs gespeichert werden.

Mit Median und relativem Schwellenwert entscheiden

Eine einzelne langsame Stichprobe sollte keinen Merge blockieren. Führen Sie für jeden Commit fünf bis sieben Runden aus, schließen Sie zunächst die Aufwärmrunde aus und berechnen Sie anschließend den Median. Als Baseline kann der Median mehrerer aktueller stabiler Läufe des Haupt-Branches dienen, statt den Wert der allerersten Messung dauerhaft beizubehalten.

Drei Arten von Veränderungen sollten getrennt betrachtet werden: Die Gesamtdauer spiegelt die Wartezeit der Benutzer wider, CPU-Metriken machen wiederholte Layout-Berechnungen oder übermäßiges Zeichnen sichtbar und Speichermetriken können Probleme mit Bild-Caches und der Wiederverwendung von Views aufdecken. Steigt bei einem Commit die Laufzeit, während CPU- und Speicherwerte stabil bleiben, sollte zunächst die Auslastung des Simulators geprüft werden. Verschlechtern sich alle drei Werte gleichzeitig, deutet dies eher auf eine Regression im Anwendungscode hin.

Der Schwellenwert sollte prozentual definiert und mit einer absoluten Mindestabweichung kombiniert werden. Ein Lauf kann beispielsweise erst dann fehlschlagen, wenn die Medianlaufzeit die Baseline um mehr als 12 % überschreitet und die absolute Zunahme zugleich über einem vom Team festgelegten Rauschpegel liegt. Die konkreten Schwellenwerte müssen aus den historischen Schwankungen derselben Umgebung abgeleitet werden und dürfen nicht unverändert aus anderen Projekten übernommen werden.

Bei Fehlern Belege sichern und Ursache eingrenzen

Wenn die Schranke auslöst, darf die Baseline nicht sofort überschrieben werden. Führen Sie zunächst denselben Commit erneut aus. Schlägt auch dieser Lauf fehl, speichern Sie xcresult, das vollständige Protokoll, die Version der Testdaten, die Versionen von Betriebssystem und Toolchain sowie Zusammenfassungen zu CPU, Speicher und Laufzeit. Grenzen Sie die Änderung anschließend anhand des Commit-Bereichs ein und prüfen Sie insbesondere Bilddecodierung, Schatten und Unschärfeeffekte, Auto-Layout-Constraints, synchrone I/O-Vorgänge im Hauptthread, Listen-Recycling und Protokollausgaben.

Außerdem muss zwischen einer „Verschlechterung der Messwerte“ und einer „Änderung des Testablaufs“ unterschieden werden. Wenn Bedienungshilfen-IDs nicht mehr funktionieren oder die Rücksetzaktion nicht zum Anfang der Liste zurückkehrt, kann der Test weiterhin ausgeführt werden, misst jedoch einen anderen Seitenbereich. Sichtbarkeits-Assertions für entscheidende Start- und Endpunkte sind wirksamer als eine bloße Erhöhung der Wiederholungsversuche.

Eine Aktualisierung der Baseline sollte als separate Änderung eingecheckt werden, die das Umgebungs-Upgrade oder eine akzeptierte Anpassung der Interaktion erläutert und Messreihen vor und nach der Aktualisierung enthält. So wird die Performance-Schranke weder durch sporadisches Rauschen ständig ausgelöst, noch verliert sie durch beiläufiges Aktualisieren der Baseline ihre Wirkung.

Häufig gestellte Fragen

Sollte die Schranke einen festen Millisekundenwert verwenden?

Nein. Ermitteln Sie eine Baseline auf derselben Maschine mit identischem System, Simulator und Datensatz und vergleichen Sie relativ mit dem Median stabiler Läufe.

Warum gehört der erste Lauf nicht in die Baseline?

Der erste Lauf kann Installation, Bibliotheksladung, Schriftinitialisierung und Cache-Aufwärmung enthalten. Ein separater Warmlauf reduziert dieses Rauschen.

Welche Artefakte sind bei einem Fehler erforderlich?

Sichern Sie xcresult, Testprotokolle, Commit, Xcode- und macOS-Version, Simulatormodell, Datensatzversion sowie CPU- und Speicherwerte.

OVPS CLOUD-MAC

Den nächsten Build auf einen dedizierten physischen Knoten verschieben

Wählen Sie aus drei Apple-Silicon-Konfigurationen und sechs verfügbaren Knoten. Maßgeblich ist der in der Konsole in Echtzeit angezeigte Status.

Konfiguration auswählen und bestellen