Lektion 4.4 · Stufe 4 – Profi

Performance messen mit Instruments

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion kannst du Leistungsprobleme messen statt raten: eine App mit Instruments profilieren, die SwiftUI-Spuren für lange body-Updates lesen, mit dem Time Profiler teure Funktionen finden, Hänger erkennen und eigene Messpunkte mit OSSignposter setzen. Du kennst den Ablauf, mit dem Profis ein Leistungsproblem eingrenzen.

Das Problem: „Es ruckelt irgendwie“

Deine App funktioniert, aber beim Scrollen durch die Liste ruckelt es leicht. Oder ein Tipp auf einen Button braucht eine gefühlte halbe Sekunde, bis etwas passiert. Was nun?

Der naheliegende Reflex: an irgendetwas drehen. LazyVStack statt VStack, ein Equatable hier, ein Task dort. Manchmal hilft es, meist nicht, und oft wird der Code dabei komplizierter, ohne schneller zu werden.

Profis gehen anders vor: Erst messen, dann ändern, dann wieder messen. Das Werkzeug dafür heißt Instruments und ist Teil von Xcode.

Die goldene Regel

Optimiere nie auf Verdacht. Leistungsprobleme sitzen fast immer an Stellen, die man nicht erwartet. Eine Messung dauert fünf Minuten und spart Stunden.

Was „langsam“ bedeutet

Ein iPhone zeichnet den Bildschirm 60- oder 120-mal pro Sekunde neu. Für jedes Bild bleiben also etwa 16 bzw. 8 Millisekunden. Alles, was auf dem Main Actor passiert (Lektion 3.7), muss in diese Zeit passen: body berechnen, Layout, Gesten verarbeiten, deinen eigenen Code ausführen.

Dauert es länger, verpasst die App ein Bild:

  • Hitch (Ruckler): Ein oder wenige Bilder fehlen. Beim Scrollen oder in Animationen sichtbar als kurzes Stocken.
  • Hang (Hänger): Der Main Actor ist für längere Zeit blockiert, ab etwa 250 Millisekunden. Die App reagiert spürbar nicht auf Eingaben.

Beide haben fast immer dieselbe Ursache: zu viel Arbeit auf dem Main Actor zur falschen Zeit.

Richtig messen

Bevor du misst, drei Regeln:

  1. Auf einem echten Gerät. Der Simulator läuft auf deinem Mac mit ganz anderer Leistung. Ein Problem, das im Simulator nicht auftritt, kann auf einem älteren iPhone deutlich spürbar sein, und umgekehrt.
  2. Im Release-Modus. Debug-Builds sind ohne Optimierungen kompiliert und teils um ein Vielfaches langsamer. Instruments verwendet beim Profilieren automatisch die Release-Konfiguration.
  3. Mit realistischen Daten. Eine Liste mit fünf Einträgen ist immer schnell. Teste mit so vielen Daten, wie echte Nutzer haben werden, gern auch mehr.

Instruments starten

  1. Schließ dein iPhone an und wähle es oben in Xcode als Ziel.
  2. Wähle Product → Profile oder drück ⌘ I. Xcode baut die App im Release-Modus und öffnet Instruments.
  3. Wähle die Vorlage SwiftUI und klick auf Choose.
  4. Klick auf den roten Aufnahme-Knopf oben links. Die App startet auf dem iPhone.
  5. Mach in der App genau das, was langsam ist: scrollen, tippen, den Bildschirm öffnen. Möglichst nur das.
  6. Stopp die Aufnahme nach ein paar Sekunden.
Screenshot folgt: Instruments mit der SwiftUI-Vorlage nach einer Aufnahme

Oben siehst du eine Zeitleiste mit mehreren Spuren übereinander, unten Details zum gewählten Bereich. Du kannst mit der Maus einen Bereich in der Zeitleiste markieren, um nur diesen Ausschnitt zu untersuchen.

Die SwiftUI-Spuren lesen

Die SwiftUI-Vorlage enthält mehrere Spuren. Die wichtigsten:

  • SwiftUI: Zeigt alle Updates, die SwiftUI durchgeführt hat. Besonders hilfreich sind die Unterspuren für lange Updates, etwa „Long View Body Updates“. Sie markieren body-Berechnungen, die so lange gedauert haben, dass sie zu einem Ruckler beitragen können. Orange und rot sind Warnsignale.
  • Time Profiler: Zeigt, welche Funktionen wie viel Rechenzeit verbraucht haben.
  • Hangs: Markiert Zeiträume, in denen der Main Actor blockiert war.

Lange body-Updates finden

  1. Klick in der SwiftUI-Spur auf einen auffälligen Bereich.
  2. Unten erscheint eine Liste der View-Typen, sortiert nach Dauer oder Anzahl der Updates.
  3. Such nach zwei Mustern:
    • Eine View mit sehr langem body: Dort passiert zu viel Arbeit, etwa Sortieren, Formatieren oder Bilder vorbereiten.
    • Eine View mit sehr vielen Updates: Sie wird viel öfter neu berechnet als nötig. Typisch, wenn sie einen schnell wechselnden Wert liest (Lektion 4.3).

In aktuellen Xcode-Versionen kannst du zu einem Update auch anzeigen lassen, was es ausgelöst hat: Instruments zeigt dann, welche State-Änderung oder welcher beobachtete Wert die Kette von Updates gestartet hat. Das ist die Antwort auf die Frage „Warum wird diese View überhaupt neu berechnet?“, ganz ohne _printChanges.

Tipp

Die genauen Namen und das Aussehen der Spuren ändern sich mit jeder Xcode-Version ein wenig. Das Prinzip bleibt: Welche View ist lang, welche View wird oft aktualisiert, und warum?

Teure Funktionen finden: der Time Profiler

Die SwiftUI-Spur sagt dir, welche View teuer ist. Der Time Profiler sagt dir, welche Funktion darin die Zeit verbraucht.

  1. Markiere den langsamen Bereich in der Zeitleiste.
  2. Wähle unten die Spur Time Profiler.
  3. Du siehst einen Aufrufbaum: welche Funktion welche andere aufgerufen hat und wie viel Zeit dabei verging.

Zwei Einstellungen unten im Bereich Call Tree machen den Baum viel lesbarer:

  • Hide System Libraries: blendet Apples Code aus und zeigt nur deinen.
  • Invert Call Tree: dreht den Baum um. Ganz oben stehen dann die Funktionen, in denen die meiste Zeit selbst verbracht wurde, die eigentlichen Übeltäter.

Ein Doppelklick auf eine Zeile öffnet den Quellcode mit der Zeit pro Zeile.

Erwartetes Ergebnis

Ein typischer Fund: In RecipeRow.body werden 40 % der Zeit in DateFormatter.init verbracht. Bei jeder Zeile und jedem Update wird ein neuer Formatierer erzeugt. Wie man das behebt, zeigt Lektion 4.5.

Hänger finden

Die Spur Hangs markiert jeden Zeitraum, in dem der Main Actor zu lange blockiert war. Markiere einen solchen Bereich und schau im Time Profiler nach, was in dieser Zeit lief. Typische Ursachen:

  • Synchrones Lesen großer Dateien oder Bilder auf dem Main Actor
  • Aufwendige Berechnungen, etwa das Sortieren und Gruppieren Tausender Einträge
  • Eine große SwiftData-Abfrage, die beim Öffnen eines Bildschirms läuft

Die Lösung ist meist, die Arbeit vom Main Actor wegzunehmen (Lektion 3.8) oder sie so zu verkleinern, dass sie nicht mehr auffällt.

Hänger findet Xcode übrigens auch ohne Instruments: Während du die App aus Xcode laufen lässt, meldet der Debug Navigator (⌘ 7) auffällige Hänger, und unter Window → Organizer → Hangs siehst du Hänger, die bei echten Nutzern aufgetreten sind, sobald die App im App Store ist.

Eigene Messpunkte: OSSignposter

Manchmal willst du genau wissen, wie lange ein bestimmter Abschnitt deines Codes dauert, etwa das Laden und Gruppieren der Ausgaben aus Projekt E. Dafür setzt du eigene Markierungen, die in Instruments als Balken erscheinen:

Swift
import OSLog

let signposter = OSSignposter(subsystem: "com.deinname.ausgaben", category: "Statistik")

func computeStatistics(for expenses: [Expense]) -> [CategorySum] {
    let state = signposter.beginInterval("Statistik berechnen")
    defer { signposter.endInterval("Statistik berechnen", state) }

    // ... die eigentliche Arbeit
    return sums
}
  • beginInterval markiert den Anfang, endInterval das Ende.
  • defer sorgt dafür, dass endInterval beim Verlassen der Funktion auf jeden Fall aufgerufen wird, egal über welches return.

In Instruments fügst du über + oben rechts die Spur os_signpost hinzu. Dort erscheint jeder Aufruf als Balken mit seiner Dauer. So siehst du schwarz auf weiß, ob eine Änderung die Funktion schneller gemacht hat.

Signposts kosten fast nichts

Solange Instruments nicht aufnimmt, sind Signposts praktisch kostenlos. Du kannst sie im Code lassen. Für normale Protokollausgaben statt print gibt es aus demselben Framework Logger, dessen Meldungen in der Konsole-App des Macs gefiltert werden können.

Der Ablauf

So gehst du bei einem Leistungsproblem vor:

  1. Reproduzieren: Finde einen Weg, das Problem zuverlässig auszulösen, etwa „Liste mit 2.000 Einträgen schnell scrollen“.
  2. Messen: Nimm es in Instruments auf. Notiere eine Zahl, zum Beispiel die Anzahl langer Updates oder die Dauer eines Signposts.
  3. Ursache finden: SwiftUI-Spur für die View, Time Profiler für die Funktion.
  4. Eine Sache ändern.
  5. Erneut messen und mit der Zahl aus Schritt 2 vergleichen.
  6. Hat es nicht geholfen: Änderung zurücknehmen. Sie macht den Code nur komplizierter.

Typische Fehler

Im Simulator oder Debug-Modus messen

Die Zahlen sind dann bedeutungslos. Miss mit ⌘ I auf einem echten Gerät.

Zu lange Aufnahmen

Eine Minute Herumklicken ergibt eine Zeitleiste, in der nichts zu finden ist. Nimm nur die paar Sekunden auf, in denen das Problem auftritt.

Alle Systemfunktionen im Aufrufbaum

Ohne Hide System Libraries und Invert Call Tree gehen deine Funktionen zwischen Tausenden Systemaufrufen unter.

Mehrere Dinge auf einmal ändern

Dann weißt du nicht, was geholfen hat und was nur den Code komplizierter gemacht hat. Immer eine Änderung, dann messen.

FAQ

Anfänger

Was bedeutet das überhaupt?

Brauche ich einen bezahlten Entwickler-Account für Instruments auf dem iPhone? Nein. Mit deiner kostenlosen Apple-ID kannst du Apps auf dein eigenes iPhone laden und dort profilieren (Lektion 0.2).

Mein iPhone ist ganz neu, da ruckelt nichts. Muss ich trotzdem messen? Deine Nutzer haben oft ältere Geräte. Was bei dir 5 Millisekunden dauert, kann auf einem vier Jahre alten iPhone 20 dauern. Wenn möglich, teste auf dem ältesten Gerät, das du unterstützen willst.

Fortgeschritten

Warum verhält es sich so?

Wie finde ich Speicherprobleme? Mit den Vorlagen Allocations (wie viel Speicher wird wofür belegt) und Leaks (Speicher, der nie freigegeben wird). Typisch sind Referenzzyklen zwischen Klassen und Closures, etwa ein Objekt, das eine Closure hält, die self stark festhält (Lektion 1.13).

Kann ich Leistung auch automatisch testen? Ja, mit Leistungstests in XCTest (measure { }), die Zeiten über mehrere Läufe mitteln und bei Verschlechterungen warnen. Für UI-Leistung gibt es Metriken wie XCTOSSignpostMetric für Scroll-Ruckler. Mehr zu Tests ab Lektion 4.10.

Profi

Was passiert unter der Haube?

Was zeigt die SwiftUI-Spur genau an? Sie zeichnet Ereignisse aus dem Inneren von SwiftUI auf: welche Attribute im Abhängigkeitsgraphen (Lektion 4.3) neu berechnet wurden, wie lange body-Aufrufe und Layout gedauert haben und welche Änderung die Kette ausgelöst hat. Lange Updates werden markiert, sobald sie einen Anteil des Bildbudgets überschreiten, sodass sie zu einem verpassten Bild beitragen können.

Wie messe ich Startzeit? Mit der Vorlage App Launch. Sie zeigt die Phasen vom Antippen des Symbols bis zum ersten Bild. Typische Bremsen sind viel Arbeit in App.init, synchrones Laden großer Daten und aufwendige erste Bildschirme. Ziel ist, dass das erste Bild in deutlich unter einer Sekunde erscheint.

Übungen

Anfänger

Übung 1: Erste Aufnahme

Profiliere eine deiner Apps, etwa den Ausgaben-Tracker aus Projekt E, mit der SwiftUI-Vorlage. Scroll ein paar Sekunden durch die Liste und öffne die Auswertung. Finde heraus, welche drei Views am häufigsten aktualisiert wurden.

Fortgeschritten

Übung 2: Den Übeltäter finden

Baue eine Liste mit 5.000 Einträgen, deren Zeilen in body jeweils einen neuen DateFormatter erzeugen und ein Datum formatieren. Profiliere sie auf einem Gerät, finde die Ursache mit dem Time Profiler (Hide System Libraries, Invert Call Tree) und notiere die Zeit, die auf den Formatierer entfällt.

Profi

Übung 3: Signposts

Füge der Auswertung aus Projekt E Signposts für das Filtern, das Gruppieren nach Kategorie und das Gruppieren nach Tag hinzu. Erzeuge mit SampleData 10.000 Ausgaben und miss, wie lange jeder Schritt dauert. Welcher Schritt ist der teuerste, und wie oft läuft er beim Wechsel des Zeitraums?

Zusammenfassung

  • Erst messen, dann ändern, dann wieder messen. Nie auf Verdacht optimieren.
  • Pro Bild bleiben etwa 8–16 ms. Zu viel Arbeit auf dem Main Actor führt zu Hitches (Ruckler) und Hangs (Hänger).
  • Messen mit ⌘ I, auf einem echten Gerät, mit realistischen Daten.
  • Die SwiftUI-Spur zeigt lange und häufige body-Updates und ihre Auslöser.
  • Der Time Profiler mit Hide System Libraries und Invert Call Tree zeigt die teuren Funktionen.
  • OSSignposter misst eigene Code-Abschnitte und macht Verbesserungen sichtbar.

Spickzettel

Swift
// Profilieren: Product → Profile (⌘ I) → Vorlage „SwiftUI“
// Time Profiler: Hide System Libraries + Invert Call Tree

import OSLog
let signposter = OSSignposter(subsystem: "com.deinname.app", category: "Daten")

func work() {
    let state = signposter.beginInterval("Arbeit")
    defer { signposter.endInterval("Arbeit", state) }
    // ...
}

let logger = Logger(subsystem: "com.deinname.app", category: "Netzwerk")
logger.info("Geladen: \(count) Einträge")

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.