Es war 2016. Trump wurde noch nicht zum Präsidenten gewählt, daher hat die # DeleteUber- Bewegung noch nicht begonnen. Travis Kalanick blieb das Geschlecht, wir erlebten eine Phase hyperaktiven Wachstums mit der Eröffnung von Filialen in anderen Ländern, die Stimmung in der Öffentlichkeit ist im Allgemeinen positiv, alle sind glücklich, Uber ist von seiner besten Seite.
Das Hyperwachstum war jedoch nicht ohne Probleme, und die Anwendung selbst begann zu versagen. Davor hat sich die Anzahl der Entwickler fast jedes Jahr verdoppelt, und wenn Sie so schnell wachsen, erhalten Sie eine unglaubliche Bandbreite an Fähigkeiten. In Kombination mit der Hacker-Mentalität, die wir "Let Builder's Build" nannten, bedeutete dies eine komplexe und fragile Anwendungsarchitektur. Zu dieser Zeit hatte die Uber-App eine extrem schwere Logik, so dass sie oft abstürzte. Wir haben ständig Hotfixes, Patches, ungeplante Releases usw. veröffentlicht. Außerdem war die Architektur nicht gut skalierbar.
Infolge all dieser Probleme begann auf allen Ebenen der Organisation eine wachsende Bewegung, die sich um die Idee versammelte, "die Anwendung von Grund auf neu zu schreiben". Ein Team wurde gebildet, um eine neue mobile Architektur für die neue Anwendung zu erstellen. Die Idee war, eine Architektur zu schaffen, die "die mobile Entwicklung von Uber in den nächsten fünf Jahren unterstützen würde". Wir haben für beide Plattformen gleichzeitig entwickelt. Der gesamte Entwicklungszyklus begann von vorne.
Die iOS-Abteilung nutzte diese Gelegenheit, um Swift (damals in Version 2.x) zu implementieren. Uber hatte Swift in der Vergangenheit ausprobiert, aber wie viele andere in diesem frühen Stadium der Technologieentwicklung gab es viele Probleme und eine verzögerte Implementierung.
Das allgemeine Gefühl war jedoch, dass die meisten Probleme von Swift zu dieser Zeit auf eine schlechte Interoperabilität mit Objective-C zurückzuführen waren. Und wenn wir eine reine Swift-Anwendung schreiben, können wir die Hauptprobleme vermeiden.
Es gab auch die Idee, die gleichen grundlegenden Architekturmuster sowohl für Android als auch für iOS zu verwenden. Android-Entwickler waren zu dieser Zeit große Fans von RxJava. Die entsprechende RxSwift-Bibliothek nutzte das funktionale Programmierparadigma in Swift. Alles schien einfach.
So beschäftigte sich ein kleines Entwicklungsteam (Design, Produkt und Architektur) mehrere Monate lang intensiv mit neuen funktionalen / reaktiven Mustern, einer neuen Sprache und einer neuen Anwendung. Alles lief gut. Die Architektur stützte sich stark auf die erweiterten Sprachfähigkeiten von Swift.
Die Benutzeroberfläche konnte auf eine große Anzahl von Uber-Apps skaliert werden, das Paradigma der funktionalen Programmierung schien leistungsfähig (wenn auch etwas schwierig zu erlernen), und die Architektur basierte auf einem neuen Echtzeit-Streaming-Netzwerkprotokoll (ich habe diesen Teil geschrieben).
Nach ein paar Monaten und mehreren beeindruckenden Demos gewann die Bewegung an Dynamik. Das Projekt sah erfolgreich aus. Mit einer kleinen Anzahl von Ingenieuren war es möglich, in kurzer Zeit hervorragende Funktionen zu entwickeln. Der größte Teil des Produkts ist fertig. Der Führer ist hübsch.
Dann begann der Einsatz für das gesamte Unternehmen. Verschiedene Teams haben begonnen, der neuen Anwendung ihre eigenen Funktionen hinzuzufügen. Die Aufregung des Neuen sorgte zunächst für eine Menge Motivation und Produktivität. Die Architektur ermöglichte die Isolierung von Funktionen, was einen schnellen Fortschritt ermöglichte.
Sobald jedoch mehr als zehn Ingenieure Swift beherrschten, begann der gut koordinierte Mechanismus auseinanderzufallen. Der Swift-Compiler ist heute noch deutlich langsamer als Objective-C, war aber damals praktisch unbrauchbar. Die Montagezeit ging vom Maßstab ab. Das Debuggen wurde vollständig gestoppt.
Irgendwo gibt es ein Video von einer der Demos, in dem ein Uber-Ingenieur eine einzeilige Anweisung in Xcode eingibt und dann 45 Sekunden wartet, bis die Buchstaben langsam nacheinander im Editor angezeigt werden.
Dann stoßen wir mit einem dynamischen Linker an eine Wand. Zu diesem Zeitpunkt konnten Swift-Bibliotheken nur dynamisch verknüpft werden. Leider lief der Linker in Polynomzeit, sodass die von Apple empfohlene maximale Anzahl von Bibliotheken in einer einzelnen Binärdatei 6 betrug. Wir hatten 92 und die Anzahl wuchs weiter ...
Daher dauerte es nach dem Klicken auf das Anwendungssymbol 8-12 Sekunden, bis überhaupt main aufgerufen wurde. Unsere glänzende neue App erwies sich als langsamer als die alte, umständliche. Dann gab es das Problem der Größe der Binärdatei.
Als sich die Probleme ernsthaft zu manifestieren begannen, hatten wir leider bereits den Punkt ohne Wiederkehr überschritten. Dies ist der logische Irrtum des Irrtums der versunkenen Kosten. Zu diesem Zeitpunkt steckte das gesamte Unternehmen seine ganze Energie in die neue Anwendung.
Tausende von Menschen aus verschiedenen Richtungen, Millionen und Abermillionen von Dollar (ich kann nicht die wirkliche Zahl angeben, aber viel mehr als eine). Das gesamte Management unterstützt das Projekt einstimmig. Ich hatte ein privates Gespräch mit meinem Chef über die Notwendigkeit aufzuhören.
Er sagte, wenn dieses Projekt scheitern würde, müsste er packen. Gleiches galt für seinen Chef bis hin zum Vizepräsidenten. Es gab keinen Ausgang.
Also haben wir die Ärmel hochgekrempelt und die besten Entwickler für jedes Problem ausgewählt, wobei kritische Themen (dynamische Verknüpfung, Binärgröße) priorisiert wurden. Mir wurde sowohl die dynamische Verknüpfung als auch die Größe der Binärdatei in dieser Reihenfolge zugewiesen.
Wir haben schnell festgestellt, dass das Verknüpfungsproblem beim Start der Anwendung behoben werden kann, indem der gesamte Code in die ausführbare Hauptdatei eingefügt wird. Wie wir alle wissen, kombiniert Swift Namespaces mit Frameworks. Daher wären umfangreiche Codeänderungen erforderlich, einschließlich unzähliger Namespace-Überprüfungen.
Zu diesem Zeitpunkt untersuchte der brillante Richard Howell die Build-Ausgabe von Xcode und stellte fest, dass er nach Abschluss des Builds alle Zwischenobjektdateien mithilfe eines benutzerdefinierten Skripts wieder in die Hauptbinärdatei verknüpfen konnte.
Da Swift den Namespace von Objekten während der Kompilierung verzerrt, bedeutet dies, dass es damit arbeiten kann. Dadurch konnten wir unsere Bibliotheken effizient statisch verknüpfen und die Startzeit von main von 10 Sekunden auf nahezu Null reduzieren.
Das nächste Problem ist die Größe. Zu diesem Zeitpunkt planten wir als Sicherheitsnetz, die neue Anwendung mit der alten zu verpacken - und sie zur Laufzeit sorgfältig bereitzustellen. Um die Größe zu reduzieren, haben wir zuerst die alte App deinstalliert. Wir haben diese Strategie "Yolo" genannt. Travis gab persönlich die Erlaubnis.
Wir haben auch alle Swift-Strukturen durch Klassen ersetzt . Werttypen verursachen aufgrund der Objektausrichtung und des zusätzlichen Maschinencodes, der zum Kopieren von Verhalten, Autoinitialisierern usw. erforderlich ist, im Allgemeinen viel Overhead. Dies spart Platz.
Aber die App wuchs weiter. Bald haben wir das Download-Limit (100 MB) von Binärdateien in iOS 8 und früher erreicht. Dies führt zu einer erheblichen Anzahl verlorener Installationen (Umsatzverluste von mehr als 10 Millionen US-Dollar aufgrund vieler iOS-Benutzer, die noch nicht aktualisiert wurden).
Zu diesem Zeitpunkt gab es einige Wochen vor dem öffentlichen Start. Wir mussten entweder zu Objective-C zurückkehren oder die Unterstützung für iOS 8 einstellen. Da iOS 9 die Möglichkeit zur Aufteilung der Architektur einführte, war diese Version tatsächlich halb so groß (Geben oder Nehmen). Als nur noch eine Woche übrig war, beschlossen wir, zig Millionen Dollar wegzuwerfen - und die Unterstützung für iOS 8 einzustellen.
Die allgemeine Meinung war, dass wir bei Halbierung der Größe viel Spielraum hatten und das Problem mit der Größe irgendwann in der Zukunft gelöst werden könnte. wenn wir den Rest harken. Leider haben wir uns sehr geirrt.
Nach der Veröffentlichung der App hatten wir eine große Party. Die App wurde von den Nutzern und der Presse gut aufgenommen. Es war schnell, mit einem hellen neuen Design.
Viele Leute wurden befördert. Wir alle atmeten erleichtert auf. Nach 90 aufeinanderfolgenden Arbeitswochen hatten die Jungs endlich eine Pause.
Aber dann begann sich die öffentliche Meinung zu ändern. Die neue App konzentrierte sich auf die Berechnung des genauen Preises einer Reise für eine bestimmte Route (früher haben Sie nur den Tarif und den aktuellen Multiplikator gesehen). Um den Preis zu berechnen, mussten Sie Ihren aktuellen Standort eingeben.
Zur Vereinfachung der Benutzer haben wir auch eine automatische Standortbestimmung installiert, die die Erfassung von Standortdaten im Hintergrund ermöglicht, sodass der Fahrer genau sehen kann, wo er den Passagier zum aktuellen Zeitpunkt abholen soll. Die Leute fingen an verrückt zu werden. Einige meiner ehemaligen Mitarbeiter auf Twitter forderten mich auf, die böse Firma zu verlassen, die solche Leute verfolgt.
Infolge dieser Unruhen wurde die Standortberechtigung in iOS deaktiviert. Die neue Anwendung sah diesen Anwendungsfall jedoch nicht vor.
Deshalb haben wir unser Bestes gegeben, um die Standardversion zurückzugeben. Wir haben besprochen, dass es möglich ist, die Standortverfolgung im Hintergrund zu deaktivieren, aber dies beeinträchtigt erneut die Benutzerfreundlichkeit, bevor Sie in ein Taxi steigen.
Dann kam Trump an die Macht (dies geschah ungefähr drei Monate nach der Veröffentlichung der neuen App), was eine Kettenreaktion auslöste, die zur # DeleteUber- Bewegung führte .
Während dieser ganzen Zeit ist die Swift-Codebasis schnell gewachsen. Laufende Probleme und eine langsame IDE haben zwei kriegführende Fraktionen unter unseren iOS-Entwicklern hervorgebracht. Ich werde sie Swift-Fanatiker und Objective-C-Nerds nennen.
Die Summe des äußeren und inneren Drucks brachte die Spannung auf das Maximum. Fanatiker bestritten Swifts Probleme. Die Bohrungen haben sich über alles Mögliche beschwert, ohne spezielle Lösungen anzubieten.
Um diese Zeit waren wir von einem Problem mit der Größe der Binärdatei betroffen. Ich war auf Abruf, als das Team Probleme mit der Veröffentlichung hatte. Es stellt sich heraus, dass unsere brillante Lösung für das Problem der dynamischen Verknüpfung eine ausführbare Datei erstellt hat, die für einige Architekturen zu groß war.
Nachdem mein Kollege @aqua_geek und ich das Problem auf diesen Architekturen gelöst hatten habe ein wenig gegraben und festgestellt, dass die Größe des kompilierten Codes mit einer Geschwindigkeit von 1,3 MB pro Woche wächst. Ich habe Alarm geschlagen. Wenn bei einer solchen Geschwindigkeit nichts unternommen wird, werden wir in drei Wochen auf das Download-Limit über das Mobilfunknetz stoßen.
Aber die innere Spannung erreichte ein solches Stadium, dass die Fanatiker alles bestritten. Einer der Technologieführer aus dem Swift-Camp schrieb einen zweiseitigen Artikel darüber, wie wichtig das Herunterladen von Mobiltelefonen nicht ist (Facebook hat es schließlich schon vor langer Zeit überschritten). Wir selbst haben es satt, Feuer zu löschen.
Einer unserer Datenwissenschaftler entwickelte daher einen Test, bei dem eine der Architekturebenen künstlich über die Grenze hinaus verschoben und die Auswirkungen auf die Geschäftsleistung gemessen wurden. In der nächsten Woche haben wir diese Ebene wieder herausgezogen und eine weitere aus dem Limit geschoben (um die Architekturen zu steuern).
Der Effekt war katastrophal. Die negativen Auswirkungen auf das Geschäft waren um mehrere Größenordnungen höher als alle Kosten der jährlichen Swift-Implementierung. Es stellt sich heraus, dass sich viele Menschen außerhalb der WLAN-Reichweite befinden, wenn sie die Uber-App zum ersten Mal herunterladen (wer hätte das gedacht?)
Also haben wir eine weitere Streikgruppe gebildet. Wir haben begonnen, die Objektdateien zu dekompilieren und Zeile für Zeile zu untersuchen, warum Swift-Code so groß geworden ist. Nicht verwendete Funktionen wurden entfernt. Tyler musste die watchOS-App wieder auf objc umschreiben.
(Die Watch-App war nur 4400 Zeilen lang, aber aufgrund der unterschiedlichen Prozessorarchitektur und mangelnder ABI-Kompatibilität müsste die gesamte Swift-Laufzeit in die App aufgenommen werden.)
Wir waren an unserer Grenze. So müde. Aber sie kamen zusammen. Damals zeigten sich wirklich brillante Ingenieure. Einer der Entwickler in Amsterdam hat herausgefunden, wie die Compiler-Optimierungsdurchläufe neu angeordnet werden können. Für diejenigen, die kein Experte für Compiler sind, werde ich erklären.
Moderne Compiler machen eine Menge Pässe. Zum Beispiel kann man Funktionen inline. Eine andere Möglichkeit besteht darin, konstante Ausdrücke durch ihre Werte zu ersetzen. Abhängig von der Ausführungsreihenfolge kann der Maschinencode kleiner oder größer sein.
Wenn Inline-Funktionen einen Wert übergeben, kann der Compiler dies erkennen und den gesamten Block ersetzen. Zum Beispiel:
int x = 3 func(x) { X + 4 }
wird nur eine Konstante 7, wenn der Compiler zuerst die Inline-Funktionen durchläuft (was viel weniger Code bedeutet).
Wenn dieser Compiler-Durchgang der zweite ist, erkennt er solche Funktionen möglicherweise nicht und Sie erhalten mehr Code. All dies hängt natürlich ganz davon ab, wie der spezifische Code aussieht, daher ist es schwierig, die Reihenfolge der Durchgänge im Allgemeinen zu optimieren.
So sagte der geniale Ingenieur aus Amsterdam, der den Algorithmus in den Release-Build einbaute, um Optimierungsdurchläufe neu zu ordnen und die Größe zu minimieren. Dies hat die Gesamtgröße des Maschinencodes um satte 11 MB verringert und uns ein wenig Zeit gegeben, uns weiterzuentwickeln.
Dieser Ansatz erschreckte jedoch die Swift-Compiler-Spezialisten. Sie befürchteten, dass nicht überprüfte Compiler-Durchläufe ungetestete Fehler aufdecken würden (obwohl jeder Durchgang an sich sicher sein sollte, ist es schwierig, über mögliche Kombinationen von Durchläufen nachzudenken). Wir haben jedoch keine größeren Probleme festgestellt.
Wir haben auch eine Reihe anderer Lösungen angewendet (Flusen für besonders teure Codevorlagen). Wir haben jeden von ihnen in der Anzahl der Entwicklungswochen gemessen, die sie uns geben. Das eigentliche Problem war jedoch die Wachstumskurve. Am Ende wurden alle Gewinne immer aufgefressen.
Am Ende hatten wir genug Zeit, um auf Apples Umzug zu warten, wodurch das Download-Limit für die Mobilfunkkommunikation auf 150 MB erhöht wurde. Sie haben auch eine Reihe von Compiler-Funktionen hinzugefügt, um die Größenoptimierung (-Osize) zu unterstützen. Nach eigenen Angaben wird Swift nach der Kompilierung niemals so klein sein wie Objective-C.
Ab diesem Jahr haben wir Swift jedoch auf das 1,5-fache der Größe des Objective-C-Maschinencodes optimiert, und schließlich hat Apple das optionale Limit erneut auf 200 MB erhöht. Das reicht aus, um uns noch ein paar Jahre am Laufen zu halten.
Aber wir haben fast versagt. Wenn Apple das Limit nicht erhöht hätte, müsste die Uber-App in ObjC zurückgeschrieben werden. Am Ende konnten wir auch andere Probleme lösen. Shiny @alanzeinound sein Team ermöglichte es, die Swift-Unterstützung in das Buck-Build-Tool aufzunehmen, wodurch die Build-Zeiten erheblich verkürzt wurden.
Wir haben unterwegs ein paar ausgebrannte Menschen verloren. Ich habe eine Menge Geld ausgegeben und harte Lektionen gelernt. Überraschenderweise bestehen die meisten bis heute darauf, dass sich das Umschreiben gelohnt hat. Die architektonische Konsistenz ist bei neuen Ingenieuren beliebt, die in das Unternehmen kommen. Sie wissen nicht einmal, wie viel Schmerz nötig war, um es zu erreichen.
Die Community hat von unserem Wissen profitiert. @ ellsk1 stellte eine erstaunliche Präsentation zusammen und machte eine Vortragsreise, um sein Wissen zu teilen. Auch ich konnte diese Erfahrung nutzen, um neuen Unternehmen und Entwicklungsteams zu helfen, bessere Entscheidungen zu treffen.
Hier ist ein Tipp. Bei der Programmierung dreht sich alles um Kompromisse. Es gibt keine allgemein bessere Sprache. Was auch immer Sie tun, verstehen Sie, was der Kompromiss ist und warum Sie ihn machen. Vermeiden Sie politische Kriege zwischen hartnäckigen Fraktionen innerhalb des Unternehmens.
Bemühen Sie sich um Fehlerstellen. Finden Sie heraus, wie Sie Kompromisse identifizieren und einen Rückzug verlassen können, wenn Sie an einem Punkt angelangt sind und feststellen, dass Sie einen Fehler gemacht haben. Ein großer Aufwand ist mit Kosten verbunden. Je später Sie jedoch den falschen Kompromiss erkennen, desto höher sind die Kosten.
Sei nicht langweilig, der nur murrt und nicht dazu beiträgt. Sei kein Fanatiker, der große Probleme für alle schafft. Die besten Ingenieure fallen in keine dieser Fallen.