Wegweiser: Wenn Haltepunkte nicht ausreichen

In einem früheren Artikel haben wir die Gründe für die Instabilität von Komponententests und den Umgang damit kennengelernt. Jetzt möchten wir eines der neuen Tools von Apple zum Debuggen und Profilieren von Code in Betracht ziehen. Wir sprechen über das auf der WWDC 2018 vorgestellte os_log-Protokollierungsframework, das durch das Leistungsanalysetool os_signpost erweitert wurde.



In einem der Sprints wurden wir beauftragt, die Generierung eines PDF-Dokuments auf der Clientseite zu implementieren. Wir haben die Aufgabe abgeschlossen und dem Team die Ergebnisse erfolgreich demonstriert. Wir wollten aber sicherstellen, dass die technischen Nuancen der Entscheidung wirksam sind. Wegweiser hat uns dabei geholfen. Damit konnten wir die Anzeige des Dokuments mehrmals beschleunigen.

Um mehr über die Anwendungstechnologie von os_signpost zu erfahren, sehen Sie, wo sie Ihnen helfen kann und wie sie uns bereits geholfen hat.

Tiefer in das Thema


Es gibt viele Anwendungen auf dem Telefon des Benutzers, und alle verwenden gemeinsame Systemressourcen: CPU, RAM, Netzwerk, Akku usw. Wenn Ihre Anwendung ihre Aufgaben ausführt und nicht abstürzt, bedeutet dies nicht, dass sie effizient und korrekt funktioniert. Im Folgenden beschreiben wir die Fälle, auf die Sie möglicherweise stoßen.

Ein suboptimaler Algorithmus kann zu einer langen CPU-Auslastung führen.

  • Zu Beginn der Anwendung fährt das System nach 20 Sekunden Wartezeit die Anwendung herunter, und der Benutzer sieht nicht einmal den ersten Bildschirm. In diesem Fall legt das System einen Absturzbericht fest, dessen Unterscheidungsmerkmal ein Ausnahmetyp ist - EXC_CRASH (SIGKILL) mit dem Typ 0x8badf00d .
  • Ressourcenintensive Prozesse im Hintergrundthread können die Reaktionsfähigkeit der Benutzeroberfläche beeinträchtigen, den Batterieverbrauch erhöhen und die Anwendung zwingen, das System zu beenden (bei längerer Überhitzung der CPU).

RAM-Fälle:

Die Spezifikationen für die Telefone auf der Apple-Website enthalten keine Informationen zum RAM. Andere Quellen bieten jedoch die folgende Speicherzuordnung für Telefonmodelle:
Typ
4S
5
5C
5s
6
6P
6S
6SP
RAM, GB
0,5
1
1
1
1
1
2
2

Typ
SE
X.
7
7P
8
8P
XS
XSM
Xr
RAM, GB
2
3
2
3
2
3
4
4
3

Wenn zu wenig freier Arbeitsspeicher vorhanden ist, sucht iOS nach Speicherplatz zum Freigeben und sendet gleichzeitig eine Speicherwarnung an alle laufenden Anwendungen. Dieser Prozess wirkt sich implizit auf die CPU und den Akku des Geräts aus. Wenn die Speicherwarnung ignoriert wird und die Speicherzuweisung fortgesetzt wird, beendet das System den Anwendungsprozess zwangsweise. Für den Benutzer sieht dies wie ein Absturz aus, ohne Rückverfolgungen im Absturzbericht.

Übermäßige Verwendung von Netzwerkanforderungen . Dies führt auch zu einer Verkürzung der Batterielebensdauer. Das Duplizieren von Anforderungen und / oder das Fehlen des Stornierens unnötiger Anforderungen führt zusätzlich zu einer ineffizienten Nutzung der CPU.

Vergessen Sie nicht CoreLocation . Je öfter und genauer wir den Standort des Benutzers abfragen, desto mehr wird der Akku des Geräts verbraucht. Um die Richtigkeit der Verarbeitung der beschriebenen Fälle zu überprüfen, empfehlen wir die Verwendung von os_signpost, um die Anwendungsprozesse zu profilieren und anschließend die erhaltenen Daten zu analysieren.

Werkzeugintegration in das Projekt


Auf der obersten Ebene besteht das Erstellen einer PDF-Datei aus drei Schritten:

  1. Empfangen von Daten über das Netzwerk;
  2. Dokumentenbildung;
  3. Anzeige auf dem Bildschirm - Wir haben beschlossen, die Phasen der Dokumentgenerierung aufzuteilen und zu protokollieren, beginnend mit dem Klicken des Benutzers auf die Schaltfläche "Generieren" und endend mit der Anzeige des Dokuments auf dem Bildschirm.

Angenommen, wir stehen vor der Aufgabe, eine asynchrone Netzwerkanforderung zu analysieren. Das Markup im Code sieht folgendermaßen aus:

import os.signpost let pointsOfInterestLog = OSLog(subsystem: "com.example.your-app", category: . pointsOfInterest) let networkLog = OSLog(subsystem: "com.example.your-app", category: "NetworkOperations") os_signpost(.event, log: pointsOfInterestLog, name: "Start work") os_signpost(.begin, log: networkLog, name: "Overall work") for element in elements { os_signpost(.begin, log: networkLog, name: "Element work") makeWork(for: element) os_signpost(.end, log: networkLog, name: "Element work") } os_signpost(.end, log: networkLog, name: "Overall work") 

Die Schritte zur Verwendung des Wegweisers sind wie folgt:

  • Importieren Sie das os.signpost-Framework.
  • Erstellen Sie eine Instanz von OSLog. Es ist zu berücksichtigen, dass es verschiedene Arten von Ereignissen gibt: Für Intervallereignisse (z. B. eine Netzwerkanforderung) können Sie eine beliebige Kategorie verwenden, und für gleichzeitige Ereignisse (z. B. Klicken auf eine Schaltfläche) die vordefinierte Kategorie pointsOfInterest / OS_LOG_CATEGORY_POINTS_OF_INTEREST.
  • Rufen Sie für Intervallereignisse die Funktion os_signpost mit den Typen .begin und .end am Anfang und am Ende der untersuchten Phase auf. Verwenden Sie für gleichzeitige Ereignisse den Typ .event.
  • Wenn der untersuchte Code asynchron ausgeführt werden kann, fügen Sie eine Wegweiser-ID hinzu, mit der Sie die Intervalle derselben Art von Vorgängen mit verschiedenen Objekten trennen können.
  • Optional können Sie den ausgelösten Ereignissen zusätzliche Daten (Metadaten) hinzufügen. Zum Beispiel die Größe der über das Netzwerk heruntergeladenen Bilder oder die Nummer der generierten PDF-Seite. Diese Informationen helfen zu verstehen, was genau in der untersuchten Phase der Codeausführung passiert.

Ähnliches gilt für obj-c:

 @import os.signpost; os_log_t pointsOfInterestLog = os_log_create("com.example.your-app",   OS_LOG_CATEGORY_POINTS_OF_INTEREST); os_log_t networkLog = os_log_create("com.example.your-app",   "NetworkOperations"); os_signpost_id_t operationIdentifier = os_signpost_id_generate(networkLog); os_signpost_event_emit(pointsOfInterestLog, operationIdentifier, "Start work"); os_signpost_interval_begin(networkLog, operationIdentifier, "Overall work"); for element in elements { os_signpost_id_t elementIdentifier = os_signpost_id_make_with_pointer(networkLog, element); os_signpost_interval_begin(networkLog, elementIdentifier, "Element work"); [element makeWork]; os_signpost_interval_end(networkLog, elementIdentifier, "Element work"); } os_signpost_interval_end(networkLog, operationIdentifier, "Overall work"); 

Zu einer Notiz. Wenn das Projekt vor Version 12.0 unter iOS ausgeführt werden soll, bietet Xcode an, os_signpost-Aufrufe in das Konstrukt if #available einzuschließen. Um den Code nicht zu überladen, können Sie diese Logik in eine separate Klasse einordnen.

Es ist zu berücksichtigen, dass os_signpost ein statisches Zeichenfolgenliteral als Parameter des Ereignisnamens benötigt. Um eine strengere Typisierung hinzuzufügen, können Sie eine Aufzählung mit Ereignistypen erstellen und diese in der Klassenimplementierung Zeichenfolgenliteralen zuordnen. Wenn Sie OSLog in eine separate Klasse einfügen, wird die Logik zum Deaktivieren für das Release-Schema hinzugefügt (hierfür gibt es einen separaten OSLog-Befehl).

 import os.signpost let networkLog: OSLog if ProcessInfo.processInfo.environment.keys.contains("SIGNPOSTS_FOR_NETWORK") { networkLog = OSLog(subsystem: "com.example.your-app", category: "NetworkOperations" } else { networkLog = .disabled } 

Sie können der Ereignismarkierung mit den folgenden Typdecodern Werte aus beliebigen Eigenschaften hinzufügen, um die Formatierung zu vereinfachen:

Werttyp
Benutzerdefinierter Bezeichner
Beispielausgabe
time_t
% {time_t} d
2016-01-12 19:41:37
Zeitwert
% {Zeitwert}. * P.
2016-01-12 19: 41: 37.774236
Zeitangabe
% {Zeitangabe}. * P.
2016-01-12 19: 41: 37.2382382823
errno
% {errno} d
Rohrbruch
iec-Bytes
% {iec-bytes} d
2,64 MiB
Bitrate
% {Bitrate} d
123 kbps
iec-Bitrate
% {iec-Bitrate} d
118 kibps
uuid_t
% {uuid_t}. * 16P
% {uuid_t}. * P.
10742E39-0657-41F8-AB99-878C5EC2DCAA

Bei der Profilerstellung der Anwendung werden nun Ereignisse von os_signpost in Form von Tabellendaten an Instruments gesendet. Verwenden Sie zum Wechseln zu Werkzeugen die Tastenkombination Cmd + I und wählen Sie dann das für die Profilerstellung erforderliche Werkzeug aus. Um die markierten Daten anzuzeigen, aktivieren Sie einfach die Werkzeuge os_signpost und Point of Interest auf der rechten Seite der Werkzeugoberfläche.



Standardmäßig werden Ereignisse in Kategorien gruppiert und in einer Tabelle angezeigt, in der ihre Anzahl und Statistiken zur Laufzeit berechnet werden. Zusätzlich gibt es eine grafische Anzeige auf der Zeitachse, die es einfach macht, die empfangenen Ereignisse mit den Ergebnissen in anderen Tools zu vergleichen. Es besteht auch die Möglichkeit, die Anzeige von Statistiken anzupassen und Expertensysteme zu schreiben - dieses Thema verdient jedoch einen separaten Artikel.

Anwendungsbeispiele


Fall Nr. 1. PDFKit vs WKWebView


Durch die Verwendung von os_signpost haben wir gesehen, dass bei kleinen Dokumenten (ein paar Seiten) der längste Schritt der letzte Schritt war - das Anzeigen des Dokuments - anstatt mit einem Netzwerk oder Grafiken zu arbeiten. Dies führte uns zu der Entscheidung, WKWebView durch PDFView zu ersetzen, wodurch die Anzeige des Dokuments von 1,5 Sekunden auf 30 Millisekunden beschleunigt wurde. In den Grafiken sieht es so aus:


Zeigen Sie ein PDF-Dokument (WKWebView) im Time Profiler an


Zeigen Sie ein PDF-Dokument (PDFView) im Time Profiler an

Die resultierenden Daten können in anderen von Xcode bereitgestellten Tools implementiert werden. Wie das Allocations-Tool zeigte, wurde der Gewinn an Download-Geschwindigkeit durch die Erhöhung des RAM-Einsatzes erzielt.

Fall Nr. 2. LowMemory-Warnung


Ein PDF-Dokument wird asynchron generiert, und für seine Erstellung muss eine erhebliche Menge an Speicher zugewiesen werden. Bei unzureichendem Speicher haben wir beschlossen, die Möglichkeit hinzuzufügen, den asynchronen Vorgang zum Erstellen eines Dokuments zu stoppen.

Wie Sie wissen, gibt die Methode cancelAllOperation bei Verwendung von NSOperationQueue eine vorhandene Warteschlange frei, stoppt jedoch nicht die bereits ausgeführten Vorgänge. Daraus schließen wir, dass es bei der Durchführung der Operation notwendig ist, ihren Zustand regelmäßig zu bestimmen und die Arbeit einzustellen. Dadurch werden Ressourcen frei, wenn der Status "Abgebrochen" aktiviert ist.

Der nächste Schritt ist eine asynchrone Operation, die wir auf Abbruch prüfen müssen. Gleichzeitig ist jedoch nicht klar, mit welcher Häufigkeit diese Prüfung durchgeführt werden soll. Wir hatten zwei Möglichkeiten - zeilenweise und seitenweise Prüfung. Auch hier hat os_signpost geholfen. Wie sich herausstellte, haben wir die Zeit zum Generieren des Dokuments (um 150 Seiten) um das Zweifache erhöht, indem wir im zeilenweisen Zyklus des Renderns der Tabelle im Dokument eine Überprüfung auf Löschung hinzugefügt haben. Die zweite Option war hinsichtlich der Leistung optimaler und verlängerte nicht die Zeit, die zum Erstellen des Dokuments benötigt wurde. Wenn wir das Speicherwarnereignis erhalten, brechen wir den Vorgang programmgesteuert ab und zeigen dem Benutzer den Fehlerbildschirm an.

Um sicherzustellen, dass der Speicher tatsächlich freigegeben wird, können wir auch os_signpost verwenden. Diesmal durch Hinzufügen einer Markierung zum Beginn des Ereignisses in der didRecieveMemoryWarning-Methode und einer Markierung zum Ende in viewDidLoad des Fehlerbildschirms. Übrigens können Sie im Simulator ein Ereignis mit unzureichendem Speicher emulieren (Umschalt + Befehl + m).

Fall Nr. 3. Einschränkungen aktualisieren


Wegweiser können im Layoutprozess hilfreich sein. Um Einschränkungen zu erstellen, verwenden wir das Mauerwerk . In der Dokumentation zum Framework heißt es, dass empfohlen wird, die Methode updateConstraints () zum Erstellen von Constrates zu verwenden. Apple rät jedoch dringend davon ab, und Sie können dies mit dem Wegweiser-Markup überprüfen.



Laut Apples Dokumentation sollten updateConstraints nur zum Ändern von Einschränkungen verwendet werden, wenn dies an dem Ort, an dem die Änderung vorgenommen wurde, nicht möglich ist.



Nach der Analyse der Ergebnisse kamen wir zu dem Schluss, dass der Aufruf von updateConstraints in unserer Anwendung nicht so häufig ist - ungefähr jedes Mal, wenn die Ansicht auf dem Bildschirm angezeigt wird.
Um mögliche Leistungsmängel zu vermeiden, empfehlen wir dennoch, die diesbezüglichen Empfehlungen von Apple zu befolgen.

Schlussfolgerungen


Im Jahr 2018 bot Apple Entwicklern die Möglichkeit, Profiling-Tools unabhängig zu erweitern. Natürlich können Sie auch andere Debugging-Tools verwenden: Haltepunkte, Ausgabe an die Konsole, Timer, benutzerdefinierte Profiler. Die Implementierung nimmt jedoch mehr Zeit in Anspruch oder vermittelt nicht immer ein vollständiges Bild des Geschehens.

Im nächsten Artikel werden wir uns überlegen, wie wir die vom Wegweiser erhaltenen Informationen effizienter nutzen können, indem wir unser eigenes Expertensystem (Custom Instruments) schreiben.



Nützliche Links


Der Artikel wurde mit @victoriaqb - Victoria Kashlina, iOS-Entwicklerin, geschrieben.

Source: https://habr.com/ru/post/de443414/


All Articles