Dans un
article précédent, nous avons appris les raisons de l'instabilité des tests unitaires et comment y faire face. Maintenant, nous voulons considérer l'un des nouveaux outils d'Apple pour le débogage et le profilage du code. Nous parlons du cadre de journalisation os_log présenté lors de la WWDC 2018, qui a été développé par l'outil d'analyse des performances, os_signpost.

Dans l'un des sprints, nous avons été chargés d'implémenter la génération d'un document pdf côté client. Nous avons terminé la tâche et démontré avec succès les résultats à l'équipe. Mais nous voulions nous assurer de l'efficacité des nuances techniques de la décision. Signpost nous a aidés. Grâce à lui, nous avons pu accélérer l'affichage du document à plusieurs reprises.
Pour en savoir plus sur la technologie d'application os_signpost, voir où cela peut vous aider et comment il nous a déjà aidé, allez sous le chat.
Plus profondément dans le problème
Il existe de nombreuses applications sur le téléphone de l'utilisateur, et toutes utilisent des ressources système communes: CPU, RAM, réseau, batterie, etc. Si votre application effectue ses tâches et ne plante pas, cela ne signifie pas qu'elle fonctionne efficacement et correctement. Ci-dessous, nous décrivons les cas que vous pourriez rencontrer.
Un algorithme sous-optimal peut entraîner une longue charge CPU.- Au démarrage de l'application, après 20 secondes d'attente, le système ferme l'application et l'utilisateur ne voit même pas le premier écran. Dans ce cas, le système définira un rapport d' erreur , dont la caractéristique distinctive sera le type d'exception - EXC_CRASH (SIGKILL) avec le type 0x8badf00d .
- Les processus gourmands en ressources dans le thread d'arrière-plan peuvent affecter la réactivité de l'interface utilisateur, augmenter la consommation de batterie et forcer l'application à mettre fin au système (en cas de surchauffe prolongée du processeur).
Boîtiers RAM:Les spécifications des téléphones sur le site Web d'Apple ne fournissent pas d'informations sur la RAM, mais d'autres sources fournissent l'allocation de mémoire suivante pour les modèles de téléphone:
Tapez
| 4S
| 5
| 5C
| 5s
| 6
| 6P
| 6S
| 6SP
|
RAM, Go
| 0,5
| 1
| 1
| 1
| 1
| 1
| 2
| 2
|
Tapez
| SE
| X
| 7
| 7P
| 8
| 8P
| XS
| XSM
| Xr
|
RAM, Go
| 2
| 3
| 2
| 3
| 2
| 3
| 4
| 4
| 3
|
Lorsqu'il y a trop peu de RAM libre, iOS commence à rechercher de la mémoire à libérer, envoyant simultanément un avertissement de mémoire à toutes les applications en cours d'exécution. Ce processus affecte implicitement le processeur et la batterie de l'appareil. Si l'avertissement de mémoire est ignoré et que l'allocation de mémoire continue, le système arrête de force le processus d'application. Pour l'utilisateur, cela ressemble à un plantage, sans trace dans le rapport de plantage.
Utilisation excessive des requêtes réseau . Cela entraîne également une diminution de la durée de vie de la batterie. La duplication des demandes et / ou l'absence d'annulation des demandes inutiles conduit en outre à une utilisation inefficace de la CPU.
N'oubliez pas CoreLocation . Plus nous demandons l'emplacement de l'utilisateur plus souvent et plus précisément, plus la batterie de l'appareil est épuisée. Pour vérifier l'exactitude du traitement des cas décrits, nous vous suggérons d'utiliser os_signpost pour profiler les processus d'application, puis d'analyser les données obtenues.
Intégration d'outils dans le projet
Au niveau supérieur, le processus de création d'un PDF se compose de trois étapes:
- recevoir des données sur le réseau;
- formation de documents;
- affichage à l'écran - nous avons décidé de diviser et d'enregistrer les étapes de génération du document, en commençant par l'utilisateur en cliquant sur le bouton "Générer" et en terminant par l'affichage du document à l'écran.
Supposons que nous soyons confrontés à la tâche d'analyser une demande de réseau asynchrone. Le balisage dans le code ressemblera à ceci:
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")
Les étapes d'utilisation de signpost sont les suivantes:
- Importez le framework os.signpost.
- Créez une instance d'OSLog. Il convient de noter qu'il existe plusieurs types d'événements: pour les événements d'intervalle (par exemple, une demande réseau), vous pouvez utiliser une catégorie arbitraire, et pour les événements simultanés (par exemple, en cliquant sur un bouton), la catégorie prédéfinie pointsOfInterest / OS_LOG_CATEGORY_POINTS_OF_INTEREST.
- Pour les événements d'intervalle, appelez la fonction os_signpost avec le type .begin et .end au début et à la fin de l'étape étudiée. Pour les événements simultanés, utilisez le type .event.
- Si le code étudié peut être exécuté de manière asynchrone, ajoutez un ID de panneau indicateur, qui vous permettra de séparer les intervalles du même type d'opérations avec différents objets.
- Facultativement, vous pouvez ajouter des données supplémentaires (métadonnées) aux événements distribués. Par exemple, la taille des images téléchargées sur le réseau ou le numéro de la page PDF générée. Ces informations aideront à comprendre ce qui se passe exactement au stade étudié de l'exécution du code.
De même sur 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");
Pour une note. Si le projet doit s'exécuter sur iOS avant la version 12.0, Xcode proposera d'encapsuler les appels os_signpost dans la construction if #available. Afin de ne pas encombrer le code, vous pouvez placer cette logique dans une classe distincte.
Il convient de noter que os_signpost nécessite un littéral de chaîne statique comme paramètre du nom de l'événement. Pour ajouter un typage plus strict, vous pouvez créer une énumération avec des types d'événements et, dans l'implémentation de classe, les mapper sur des littéraux de chaîne. Mettre OSLog dans une classe distincte ajoutera la logique pour le désactiver pour le schéma de publication (il existe une commande OSLog distincte pour cela).
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 }
Vous pouvez ajouter des valeurs de n'importe quelle propriété au marquage d'événement avec les décodeurs de type suivants pour une mise en forme pratique:
Type de valeur
| Spécificateur personnalisé
| Exemple de sortie
|
time_t
| % {time_t} d
| 2016-01-12 19:41:37
|
timeval
| % {timeval}. * P
| 2016-01-12 19: 41: 37.774236
|
timepec
| % {timespec}. * P
| 2016-01-12 19: 41: 37.2382382823
|
errno
| % {errno} d
| Pipe cassée
|
octets iec
| % {iec-bytes} d
| 2,64 Mio
|
débit binaire
| % {bitrate} d
| 123 kbps
|
iec-bitrate
| % {iec-bitrate} d
| 118 kibps
|
uuid_t
| % {uuid_t}. * 16P % {uuid_t}. * P
| 10742E39-0657-41F8-AB99-878C5EC2DCAA
|
Désormais, lors du profilage de l'application, les événements de os_signpost seront envoyés à Instruments sous forme de données tabulaires. Pour passer aux outils, utilisez le raccourci clavier Cmd + I, puis sélectionnez l'outil nécessaire pour le profilage. Pour voir les données marquées, il suffit d'activer les outils os_signpost et Point of Interest sur le côté droit de l'interface de l'outil.

Par défaut, les événements sont regroupés en catégories et affichés dans un tableau, où leur nombre et leurs statistiques sur l'exécution sont calculés. De plus, il y a un affichage graphique sur la chronologie, ce qui facilite la comparaison des événements reçus avec les résultats d'autres outils. Il est également possible de personnaliser l'affichage des statistiques et de rédiger des systèmes experts - mais ce sujet mérite un article séparé.
Exemples d'utilisation
Cas n ° 1. PDFKit vs WKWebView
Grâce à l'utilisation d'os_signpost, nous avons vu que pour les petits documents (quelques pages), l'étape la plus longue était la dernière - afficher le document - plutôt que de travailler avec un réseau ou des graphiques. Cela nous a conduit à la décision de remplacer
WKWebView par
PDFView , ce qui a accéléré l'affichage du document de 1,5 seconde à 30 millisecondes. Sur les graphiques, cela ressemble à ceci:
Afficher un document PDF (WKWebView) dans Time Profiler
Afficher un document PDF (PDFView) dans Time ProfilerLes données résultantes peuvent être implémentées dans d'autres outils fournis par Xcode. Comme l'a montré l'outil Allocations, le gain de vitesse de téléchargement a été obtenu en augmentant l'utilisation de la RAM.
Cas n ° 2. Avertissement de faible mémoire
Un document PDF est généré de manière asynchrone et sa formation nécessite l'allocation d'une quantité importante de mémoire. En cas de mémoire insuffisante, nous avons décidé d'ajouter la possibilité d'arrêter l'opération asynchrone de création d'un document.
Comme vous le savez, lorsque vous utilisez NSOperationQueue, la méthode cancelAllOperation libère une file d'attente existante, mais n'arrête pas les opérations déjà en cours d'exécution. De cela, nous concluons que dans la mise en œuvre de l'opération, il est nécessaire de déterminer périodiquement son état et de cesser de fonctionner. Libérant ainsi des ressources s'il est défini sur le statut Annulé.
La prochaine étape est une opération asynchrone dont nous devons vérifier l'annulation. Mais en même temps, la fréquence de cette vérification n'est pas claire. Nous avions deux options - la vérification ligne par ligne et la vérification page par page. os_signpost a également aidé ici. En fin de compte, en ajoutant un contrôle d'annulation dans le cycle ligne par ligne de rendu du tableau dans le document, nous avons augmenté de 2 fois le temps nécessaire pour générer le document (de 150 pages). La deuxième option était plus optimale en termes de performances et n'a en fait pas augmenté le temps de création du document. Par conséquent, lorsque nous recevons l'événement d'avertissement de mémoire, nous annulons l'opération par programme et affichons l'écran d'erreur pour l'utilisateur.
Pour vous assurer que la mémoire est bien libérée, nous pouvons également utiliser os_signpost. Cette fois, en ajoutant un marqueur sur le début de l'événement dans la méthode didRecieveMemoryWarning et un marqueur sur la fin dans le viewDidLoad de l'écran d'erreur. Soit dit en passant, vous pouvez émuler un événement de mémoire insuffisante dans le simulateur (shift + commande + m).
Cas n ° 3. Mettre à jour les contraintes
Le panneau peut être utile dans le processus de mise en page. Pour créer des contraintes, nous utilisons le cadre de
maçonnerie . La documentation du framework indique qu'il est recommandé d'utiliser la méthode updateConstraints () pour créer des interprétations. Mais Apple déconseille fortement de le faire, et vous pouvez le vérifier avec le balisage du panneau.

Selon la documentation d'Apple, updateConstraints ne doit être utilisé pour modifier les contraintes que si nous ne pouvons pas le faire à l'endroit où le changement s'est produit.

Après avoir analysé les résultats, nous avons conclu que dans notre application, l'appel updateConstraints n'est pas si fréquent - environ chaque fois que la vue apparaît à l'écran.
Malgré cela, afin d'éviter d'éventuels défauts de performances, nous vous recommandons de suivre les conseils d'Apple à ce sujet.
Conclusions
En 2018, Apple a donné aux développeurs la possibilité d'étendre indépendamment les outils de profilage. Bien sûr, vous pouvez utiliser d'autres outils de débogage: points d'arrêt, sortie vers la console, minuteurs, profileurs personnalisés. Mais ils prennent plus de temps à mettre en œuvre ou ne donnent pas toujours une image complète de ce qui se passe.
Dans le prochain article, nous verrons comment utiliser plus efficacement les informations reçues de signpost en écrivant notre propre système expert (Custom Instruments).
Liens utilesL'article a été écrit avec @victoriaqb - Victoria Kashlina, développeur iOS.