Comment Uber a réécrit l'application iOS avec Swift

Alors, amis, asseyez-vous en cercle et écoutez l'histoire de la plus grande catastrophe technique à laquelle j'ai participé. C'est une histoire sur la politique, l'architecture et l'erreur logique des coûts irrécupérables (désolé, je ne fais que boire du Scotch Single Malt Aberlour Cask Strength en ce moment).





C'était en 2016. Trump n'a pas encore été élu président, donc le mouvement #DeleteUber n'a pas encore commencé. Travis Kalanick est resté un genre, nous vivions une phase de croissance hyperactive avec l'ouverture de succursales dans d'autres pays, le sentiment public est globalement positif, tout le monde est content, Uber est à son meilleur.



Mais l'hyper-croissance n'a pas été sans problèmes, et l'application elle-même a commencé à mal fonctionner. Avant cela, le nombre de développeurs doublait presque chaque année, et lorsque vous grandissez aussi vite, vous obtenez une gamme incroyable de compétences. Combiné à la mentalité de hacker que nous appelions «Let builder's build», cela signifiait une architecture d'application complexe et fragile. À cette époque, l'application Uber avait une logique extrêmement lourde, elle tombait donc souvent en panne. Nous publions constamment des correctifs, des correctifs, des versions non planifiées, etc. De plus, l'architecture ne s'est pas bien adaptée.



À la suite de tous ces problèmes, un mouvement croissant a commencé à tous les niveaux de l'organisation qui s'est rallié autour de l'idée de «réécrire l'application à partir de zéro». Une équipe a été formée pour créer une nouvelle architecture mobile pour la nouvelle application. L'idée était de créer une architecture qui «soutiendrait le développement mobile d'Uber au cours des cinq prochaines années». Nous avons développé pour les deux plates-formes à la fois. Tout le cycle de développement a recommencé.



Le département iOS en a profité pour implémenter Swift (alors en version 2.x). Uber avait essayé Swift dans le passé, mais comme tant d'autres à ce stade précoce du développement de la technologie, il a rencontré de nombreux problèmes et retardé sa mise en œuvre.



Cependant, le sentiment général était que la plupart des problèmes de Swift à l'époque étaient dus à une mauvaise interopérabilité avec Objective-C. Et si nous écrivons une application Swift pure, nous pourrions éviter les principaux problèmes.



Il y avait également une idée d'utiliser les mêmes modèles architecturaux de base sur Android et iOS. Les développeurs Android étaient de grands fans de RxJava à l'époque. La bibliothèque RxSwift correspondante a tiré parti du paradigme de programmation fonctionnelle de Swift. Tout semblait simple.



Ainsi, une petite équipe de développement (Design, Produit et Architecture) est allée tête baissée dans de nouveaux modèles fonctionnels / réactifs, un nouveau langage et une nouvelle application pendant plusieurs mois. Tout allait bien. L'architecture reposait fortement sur les capacités linguistiques avancées de Swift.



L'interface utilisateur pouvait évoluer vers un grand nombre d'applications Uber, le paradigme de programmation fonctionnelle semblait puissant (bien qu'un peu difficile à apprendre) et l'architecture était basée sur un nouveau protocole de réseau de streaming en temps réel (j'ai écrit cette partie).



Après quelques mois et plusieurs démos marquantes, le mouvement a pris de l'ampleur. Le projet avait l'air réussi. Avec un petit nombre d'ingénieurs, il a été possible de développer d'excellentes fonctionnalités en peu de temps. La plupart du produit est prêt. Le manuel est joli.



Puis le déploiement dans toute l'entreprise a commencé. Diverses équipes ont commencé à ajouter leurs propres fonctionnalités à la nouvelle application. Au début, l'excitation du nouveau a créé une vague de motivation et de productivité. L'architecture prévoyait l'isolement des fonctions, ce qui permettait une évolution rapide.



Mais dès que plus de dix ingénieurs ont maîtrisé Swift, le mécanisme bien coordonné a commencé à s'effondrer. Le compilateur Swift est encore nettement plus lent qu'Objective-C aujourd'hui, mais était alors pratiquement inutilisable. Le temps de montage a déraillé. Le débogage est complètement arrêté.



Quelque part, il y a une vidéo de l'une des démos, où un ingénieur Uber tape un opérateur sur une ligne dans Xcode, puis attend 45 secondes que les lettres apparaissent lentement, une par une, dans l'éditeur.



Ensuite, nous frappons un mur avec un éditeur de liens dynamique. À l'époque, les bibliothèques Swift ne pouvaient être liées que dynamiquement. Malheureusement, l'éditeur de liens fonctionnait en temps polynomial, donc le nombre maximum recommandé par Apple de bibliothèques dans un seul fichier binaire était de 6. Nous en avions 92, et le nombre ne cessait de croître ...



En conséquence, après avoir cliqué sur l'icône de l'application, il a fallu 8 à 12 secondes avant même d'appeler main. Notre nouvelle application brillante s'est avérée être plus lente que l'ancienne. Ensuite, il y avait le problème de la taille du binaire.



Malheureusement, lorsque les problèmes ont commencé à se manifester sérieusement, nous avions déjà dépassé le point de non-retour. C'est l'erreur logique de l'erreur de coût irrécupérable. À ce stade, toute l'entreprise mettait toute son énergie dans la nouvelle application.



Des milliers de personnes de différentes directions, des millions et des millions de dollars (je ne peux pas donner le chiffre réel, mais bien plus d'un). Toute la direction est unanime pour soutenir le projet. J'ai eu une conversation privée avec mon patron sur la nécessité d'arrêter.



Il a dit que si ce projet échouait, il devrait faire ses valises. La même chose était vraie pour son patron jusqu'au vice-président. Il n'y avait pas de sortie.



Nous avons donc retroussé nos manches et mis les meilleurs développeurs sur chaque problème, en priorisant les problèmes critiques (liaison dynamique, taille binaire). On m'a attribué à la fois la liaison dynamique et la taille du binaire, dans cet ordre.



Nous avons rapidement découvert que le problème de liaison au démarrage de l'application pouvait être résolu en plaçant tout le code dans l'exécutable principal. Mais comme nous le savons tous, Swift combine les espaces de noms avec des frameworks; donc d'énormes changements de code seraient nécessaires, y compris d'innombrables vérifications d'espace de noms.



C'est alors que le brillant Richard Howell a examiné la sortie de construction de Xcode et a découvert qu'une fois la construction terminée, il pouvait prendre tous les fichiers objets intermédiaires et les relier à nouveau dans le binaire principal à l'aide d'un script personnalisé.



Puisque Swift gère l'espace de noms des objets lors de la compilation, cela signifie qu'il peut y opérer. Cela nous a permis de lier efficacement nos bibliothèques de manière statique et de réduire le temps de démarrage de main de 10 secondes à presque zéro.



Le problème suivant est la taille. À ce moment-là, comme filet de sécurité, nous avions prévu de conditionner la nouvelle application avec l'ancienne - et de la déployer soigneusement lors de l'exécution. Pour réduire la taille, la première chose que nous avons faite a simplement été de désinstaller l'ancienne application. Nous avons appelé cette stratégie "Yolo". Travis a personnellement donné son feu vert.



Nous avons également remplacé toutes les structures Swift par des classes . Les types de valeur entraînent généralement beaucoup de surcharge en raison de l'alignement des objets et du code machine supplémentaire requis pour le comportement de copie, les initialiseurs automatiques, etc.



Mais l'application a continué à se développer. Nous avons rapidement atteint la limite de téléchargement (100 Mo) de binaires dans iOS 8 et les versions antérieures. Cela se traduit par un nombre important d'installations perdues (10 + millions de dollars de revenus perdus en raison de nombreux utilisateurs iOS non encore mis à jour).



À ce stade, il y avait plusieurs semaines avant le lancement public. Nous avons dû soit revenir à Objective-C, soit abandonner le support d'iOS 8. Depuis qu'iOS 9 a introduit la possibilité de diviser l'architecture, cette version était en fait la moitié de la taille (donner ou prendre). Lorsqu'il ne restait plus qu'une semaine, nous avons décidé de jeter des dizaines de millions de dollars - et d'abandonner le support d'iOS 8.



L'opinion générale était que lorsque la taille était réduite de moitié, nous avions beaucoup de marge de manœuvre et le problème de la taille pourrait être résolu dans le futur. quand nous ratissons le reste. Malheureusement, nous nous sommes trompés.



Après la sortie de l'application, nous avons organisé une grande fête. L'application a été bien accueillie par les utilisateurs et la presse. C'était rapide, avec un nouveau design audacieux.



Beaucoup de gens ont été promus. Nous avons tous poussé un soupir de soulagement. Après 90 semaines consécutives de travail, les gars ont enfin eu une pause.



Mais alors l'opinion publique a commencé à changer. La nouvelle application se concentrait sur le calcul du prix exact d'un trajet pour un itinéraire spécifique (autrefois, vous voyiez simplement le tarif et le multiplicateur actuel). Pour calculer le prix, vous deviez entrer votre emplacement actuel.



Pour la commodité des utilisateurs, nous avons également installé la détermination automatique de l'emplacement, permettant la collecte de données de localisation en arrière-plan afin que le conducteur puisse voir exactement où prendre le passager à l'heure actuelle. Les gens ont commencé à devenir fous. Certains de mes anciens collègues sur Twitter m'ont exhorté à quitter la société perverse qui suit les gens comme ça.



À la suite de ces troubles, les gens ont commencé à désactiver l'autorisation de localisation dans iOS. Mais la nouvelle application ne prévoyait pas ce cas d'utilisation.



Nous avons donc fait de notre mieux pour retourner la version standard. Nous avons discuté de la possibilité de désactiver le suivi de l'emplacement en arrière-plan, mais cela gâche encore une fois la convivialité avant de monter dans un taxi.



Puis Trump est arrivé au pouvoir (cela s'est produit environ trois mois après la sortie de la nouvelle application), ce qui a déclenché une réaction en chaîne qui a conduit au mouvement #DeleteUber .



Pendant tout ce temps, la base de code Swift s'est développée rapidement. Des problèmes persistants et un IDE lent ont engendré deux factions en guerre parmi nos développeurs iOS. Je les appellerai fanatiques de Swift et nerds d'Objective-C.



La somme des pressions externe et interne a porté la tension au maximum. Les fanatiques ont nié les problèmes de Swift. Les alésages se sont plaints de tout ce qui était imaginable sans proposer de solutions particulières.



À cette époque, nous avons été frappés par un problème avec la taille du binaire. J'étais sur appel lorsque l'équipe a eu des problèmes avec la version. Il s'avère que notre solution brillante au problème de liaison dynamique a créé un exécutable trop volumineux pour certaines architectures.



Ayant résolu le problème sur ces architectures, mon collègue @aqua_geek et moia fait un peu de fouille et a constaté que la taille du code compilé augmente à un rythme de 1,3 Mo par semaine. J'ai sonné l'alarme. Si rien n'est fait, à une telle vitesse, nous atteindrons la limite de téléchargement sur le réseau cellulaire dans trois semaines.



Mais la tension interne a atteint un tel stade que les fanatiques ont tout nié. L'un des leaders technologiques du camp Swift a écrit un article de deux pages sur l'importance des limites de téléchargement cellulaire (Facebook, après tout, l'a dépassé il y a longtemps). Nous sommes nous-mêmes fatigués d'éteindre les incendies.



Par conséquent, l'un de nos scientifiques des données a développé un test en déplaçant artificiellement l'une des couches architecturales en dehors de la limite - et en mesurant l'impact sur les performances de l'entreprise. La semaine suivante, nous avons retiré cette couche et en avons poussé une autre hors des limites (pour contrôler les architectures).



L'effet a été désastreux. L'impact négatif sur l'activité s'est avéré être plusieurs ordres de grandeur supérieur à tous les coûts de la mise en œuvre annuelle de Swift. Il s'avère que beaucoup de gens sont hors de portée du WiFi lorsqu'ils téléchargent l'application Uber pour la première fois (qui l'aurait pensé?)



Nous avons donc formé un autre groupe de grève. Nous avons commencé à décompiler les fichiers objets et à examiner ligne par ligne pour déterminer pourquoi le code Swift était devenu si volumineux. Suppression des fonctions inutilisées. Tyler a dû réécrire l'application watchOS en objc.



(L'application de la montre ne faisait que 4400 lignes, mais en raison de l'architecture de processeur différente et du manque de compatibilité ABI, l'intégralité de l'exécution de Swift devrait être incluse dans l'application.)



Nous étions à notre limite. Tellement fatigué. Mais ils se sont réunis. C'est alors que des ingénieurs vraiment brillants se sont montrés. L'un des développeurs à Amsterdam a trouvé comment réorganiser les passes d'optimisation du compilateur. Pour ceux qui ne sont pas experts en compilateurs, je vais vous expliquer.



Les compilateurs modernes font une tonne de passes. Par exemple, on peut intégrer des fonctions. Une autre consiste à remplacer les expressions constantes par leurs valeurs. Selon l'ordre d'exécution, le code machine peut être plus petit ou plus grand.



Si les fonctions en ligne passent une valeur, le compilateur peut la reconnaître et remplacer le bloc entier. Par exemple:



int x = 3
func(x) {
X + 4
}
      
      





devient juste une constante 7 si le compilateur passe d'abord par les fonctions en ligne (ce qui signifie beaucoup moins de code).



Si cette passe du compilateur est la seconde, il se peut qu'il ne reconnaisse pas ces fonctions et vous obtiendrez plus de code. Tout cela, bien sûr, dépend entièrement de l'apparence du code spécifique, il est donc difficile d'optimiser l'ordre des passes en général.



C'est ce qu'a déclaré un brillant ingénieur d'Amsterdam, qui a construit un algorithme dans la version de la version pour réorganiser les passes d'optimisation et minimiser la taille. Cela a pris 11 Mo sur la taille totale du code machine et nous a donné un peu de temps pour continuer à développer.



Mais cette approche a horrifié les spécialistes du compilateur Swift, ils craignaient que des passes non vérifiées du compilateur révèlent des bogues non vérifiés (bien que chaque passe devrait être intrinsèquement sûre, il est difficile de raisonner sur les combinaisons possibles de passes). Cependant, nous n'avons rencontré aucun problème majeur.



Nous avons également appliqué un tas d'autres solutions (linting pour les modèles de code particulièrement coûteux). Nous avons mesuré chacun d'eux en fonction du nombre de semaines de développement qu'ils nous accordent. Mais le vrai problème était la courbe de croissance. En fin de compte, tous les gains ont toujours été mangés.



En conséquence, nous avons eu suffisamment de temps pour attendre le déménagement d'Apple, qui a augmenté la limite de téléchargement via la communication cellulaire à 150 Mo. Ils ont également ajouté un certain nombre de fonctions de compilateur pour aider à l'optimisation de la taille (-Osize). De leur propre aveu, Swift ne sera jamais aussi petit après compilation qu'Objective-C.



Mais à partir de cette année, nous avons optimisé Swift à 1,5 fois la taille du code machine Objective-C, et finalement Apple a de nouveau relevé la limite optionnelle à 200 Mo. Cela suffit pour nous permettre de continuer encore quelques années.



Mais nous avons failli échouer. Si Apple n'avait pas augmenté la limite, l'application Uber aurait dû être réécrite dans ObjC. En fin de compte, nous avons également pu résoudre d'autres problèmes. Brillant @alanzeinoet son équipe a permis d'inclure le support Swift dans l'outil de construction Buck, ce qui a considérablement réduit les temps de construction.



Nous avons perdu un tas de gens épuisés en cours de route. J'ai dépensé une tonne d'argent et appris de dures leçons. Étonnamment, à ce jour, la plupart insistent sur le fait que la réécriture en valait la peine. La cohérence architecturale est appréciée des nouveaux ingénieurs qui viennent dans l'entreprise. Ils ne savent même pas combien de douleur il a fallu pour y parvenir.



La communauté a bénéficié de nos connaissances. @ ellsk1 a réalisé une présentation incroyable et a fait une tournée de conférences pour partager ses connaissances. J'ai moi aussi pu tirer parti de cette expérience pour aider les nouvelles entreprises et les équipes de développement à prendre de meilleures décisions.



Alors, voici une astuce. Tout dans la programmation est une question de compromis. Il n'y a pas de langage universellement meilleur. Quoi que vous fassiez, comprenez quel est le compromis et pourquoi vous le faites. Évitez la guerre politique entre factions tenaces au sein de l'entreprise.



Efforcez-vous aux points de défaillance. Trouvez comment identifier les compromis et quittez une retraite si vous arrivez à un point et réalisez que vous avez commis une erreur. Beaucoup d'efforts ont un coût, mais plus tard vous réalisez le mauvais compromis, plus le coût est élevé.



Ne soyez pas ennuyeux qui grogne et ne contribue pas. Ne soyez pas un fanatique qui crée de gros problèmes pour tout le monde. Les meilleurs ingénieurs ne tombent dans aucun de ces pièges.



All Articles