Uber uses Swift to rewrite app's pit Experience and Solutions (reprint)

Source: Internet
Author: User

This article is from Uber Mobile architecture and framework group leader Thomas Atman in 2016 at the Wan District Swift Summit, sharing the good and bad of using Swfit to rewrite Uber. The following are the translations:

I am Thomas Atman, currently the head of the Uber Mobility architecture and framework group. Uber is now up to millions of users, so how does Uber work with the framework?

the story of Swift and hundreds of engineers-reasons, architecture, experience

Today I want to talk about how more than 100 Uber engineers are using the swift programming language, and all of the new Rider app main applications were reconstructed in swift language in Wednesday. My next share consists of three parts: The reason for choosing Swift, the new Uber architecture, and the refactoring experience.

the beginning of uber-the reason for refactoring

That's what the entire mobile team looked like four years ago (pointing to a screen showing three engineers), and that's when they started building the foundation of our current old application. The old application has been in stable use for four years, but due to the exponential growth of the mobile development team, the drawbacks of this architecture have gradually been shown, and it is becoming increasingly difficult to do functional development based on this old architecture. Because a lot of viewcontroller are shared with different teams, the other code needs to be tested every time. The main reason that the old architecture really crashes us is that it was written by two engineers, but the team has grown to more than 100 people today. At the same time, the number of users of the product itself is not very large. We've started running in multiple cities, and the problem with the dense bottom of the product slider is also shown because all teams want to be able to launch new products in their city. We also want to make a new user experience interface for the rider app. Based on these problems, in fact, this is the current application of the architectural problems and user experience interface of the new design issues. The future is no longer to study the old architecture and then to solve the problem in this form, but everything starts from scratch.

2015 has done a lot of correcting the wrong work, trying to improve the old structure, but the new design of Uber will fundamentally solve the problem, and will be in a more secure stage, re-design from scratch is also the best way to solve the problem.

The goal of the refactoring architecture-stable and reliable and supporting future development

Based on these two refactoring reasons, the development of the new architecture is started. The most basic requirement is to meet the above two requirements, to ensure the stability of the four core processes, which basically means that the crash rate is at the lowest level. If your application does not crash, but the user still stays on some screens, it is obvious that the problem is significant, which makes the user feel unreliable.

We also hope that the newly developed architecture will support Uber's development over the next few years, just as the old architecture was designed to meet the past four years of development.

Swift has become our choice .

To achieve the above two goals, we chose Swift. We thought Swift was safer at the time, at least in the imagination, but no one in real life was going to verify that.

We think that type safety in the compiler exposes the problem earlier, rather than waiting for the product to come online and then have problems.

And we know that four years from now, Swift will enter a period of golden development, and it will become the only language that Apple has to vigorously promote in the future.

Time Line

Starting from the beginning of this year, in February, we were hoping that what we were doing was right, because some engineers spent a lot of time doing refactoring in the old company, but eventually they ended up failing. In order to ensure the success of the restructuring, the selection of a few core engineers, so that they spent 5 months to study the old architecture, in this 5 months time, we only do this one thing: architecture, framework, the completion of a number of basic work, and finally set up a perfect foundation framework, All are developed using this basic framework as a prototype.

In June, the architecture was built and the core flow team began to use it. The core stream is going to take a new UberX or UberPool ride, so we've added 20 engineers and spent two months reviewing the new architecture to make sure that what we're proposing is consistent with the requirements for building a new product. It turns out that something is missing from the initial product requirements, such as in the view layer, once the engineer starts to convert or does some complex view operations, then we have to adjust the architecture to meet their needs. But after two months, we have made new progress, we no longer need to do a lot of migration to the codebase, and have opened the platform to everyone, and if they need it, they can transfer their functionality.

New Architecture

The new architecture is called "Riblets", which is composed of the core components of router, Interaction, Builder, Presenter, and view, which is also an idea of the Viper framework. We studied Viper, MVVM, and MVC, and the final proposal was to add some of our own innovative elements to the Viper Foundation. The ultimate goal is to modularize each function, and each module can be tested independently. Each of the core components in the Riblet framework has a protocol interface, so developers can take each unit out individually and test it thoroughly. Each module in the Riblets framework is managed within the tree, so there is no state machine and instead a state tree. Each node in the state tree is a riblet, and the core part of the new architecture is based on the business logic, not the view logic, and all the business logic is locally determined.


Take the "register" module in this tree diagram as an example, do not know whose parent node is, but what it needs has been injected into it, it is the parent node to inject it needs to rely on things, there may be a listener is listening to the registration stream, but the listener is not aware of the registration module is located in the tree. So, these modules are completely independent, and each individual module makes local decisions. For example, from the "app" module, it is only responsible for a business module: "The current system has a session token", which is the only thing it listens to, if the APP module found that there is no session in the stream, it will point the path to "Welcome" place If it finds a session, it skips over the "Welcome" module and goes directly to the "Bootstrap" module.

After that, each component on the right side of the tree is aware that the system is currently in the "logged in" state, and they all have a token that can be taken from the standalone injection to the session token, and there is no need to care if the user has exited. If a network phone suddenly comes in at one of the nodes below, and eventually causes the session to be invalid, then the app component will be heard, it will be called through the stream, and then know that the system is currently no session state, and then the app component will break the bootstrap tree, And eventually point the stream to the welcome component.

This allows different teams to focus on only the part of the business that they are responsible for, and there is no need to say that each step needs to communicate with other teams. Each team can make its own local decisions, and the dependencies are always met.

Multi-line code inside multiple files

A lot of code is generated during the development process, and we define the protocol between each module. Some components associate a riblit with 5 different files, so there will be more than 5,000 files in the codebase, along with more than 500,000 lines of code. There are also some core components that are used by the objective-c, which is also completely no problem.

experience in the learning process

We have also gained some experience in the process of learning Swift.

    • The good side

It is clear that Swift is a better language, and that is why we have a very good start, and we almost use all the features that Swift provides.

1. Reliability

The reliability of Swift is the first surprise that it has brought to us, and it seems to me that within four months of the framework's development, I suddenly realized that my integrated development tools and my applications did not crash during the entire development process, even in debug mode. I asked the other members of the team, and their answer was no crash. During the entire development process, the first crash was when we tried to use a 32-bit machine, resulting in an integer overflow parsing the JSON. That was the first crash that occurred throughout the development cycle.


We were very excited about the reliability of Swift, and the final data showed that the absolute failure rate was 99.99%, which was close to 100%. There was almost no crash in the first run of an application, which I had never encountered before.

One thing that must be considered is that no one else can be allowed to decompress the new application unconditionally, and because of this, there will be no absolute failure rate of 99.99%, so we put a linting in it, thus ensuring that nobody is able to decompress under any conditions.

You have to consider all the critical situations, as if you write a lot of if, but there is no corresponding else, that the application can have an exception, so in the debugging phase must use the declaration, the final line needs to be removed, so that the application will rarely crash.

    • The bad side.

Now we need to say something awful, but it makes sense if you can grow from failure and adversity.

1. Tough Tests

First of all, how to test is a very difficult thing. Swift is a static language, so there is no way to rely on mock test frameworks for testing as in OBJECTIVE-C development. Since all development is based on protocol, and the Protocol is primarily on our side, we have to find a test scenario for these protocols. For example, this protocol is used to create an interface for the implementation class, which allows you to save data based on a key, and allows you to retrieve data based on that key, and if you have an interaction, you want to test some business logic, such as when it gets some input value, If you can save these values to your hard drive, you have to have an implementation, and you have to have a page that simulates this storage scenario, so that you can test which method is called. We started to create these simulations manually, and began to write the code, and eventually we got the conclusion that it was not extensible. We cannot provide support for multiple engineers.

All we do is generate a small script that is responsible for converting the big script to a small script. There are some problems in it, but in the end we all get rid of it, no matter what link you want to generate the test content, just introduce script/generate-mocks. It will be through your source code, go to the agreement to find the statements with @createmock, I hope that swift to some extent to provide us with attributes, and create mocks for you. So when you run through a code base, the protocol eventually becomes a storingmock, which implements storage. What it does is implement all the common methods in the protocol. If you want to know how many times this protocol has been called, it also provides a count function. It will implement all the actual methods for you, whenever it is possible to return a default type. For example, in Dataforkey, you have an optional nsdata, and the mock only returns nil because it is perfect. It conforms to the interface and if you want to sort test your input, you can also call Dataforkeyhandlers at any time, set it to off, and test you to get the correct input from the test.

The same principle, Storagedataforkey returns a Storageresult, which is an enumeration type, which, by default, returns the first member of an enumeration. The problem of the test work is solved, and can also generate all the mock, I probably forget, the generated mock about 100,000 lines, these 100,000 lines are completely automatic generation, it is not necessary for us to manually hit the code.

2. Tool issues

Another bad thing is the problem of developing tools. We call this "infinite index". I don't know why this happens, and maybe you've come across that the indexer is indexing all the time. I don't know why, it just can't finish indexing work. At the same time it has a negative impact on CPU usage of up to 328%, so that the notebook will become hot, in the case of not plugged in, the notebook will probably only be used for 1.5 hours. This is a strange thing, because the code is growing every day, and this problem is becoming more and more serious. We have not encountered these problems before, but once we have exceeded 200000 or 300000 lines of code, the problem will become even more serious.

In addition, the IDE starts doing this: (the screen shows Xcode's video, slowly typing the string). It's not that I'm typing slowly, but I've typed the entire string, but the IDE checks every key stroke with sourcekit, and it doesn't matter if the code I wrote is correct, and you can't type at all.

Reference

Solution:

If you run into this problem, you might as well do this (the screen shows the video to remove Xcode). You can switch to other applications, such as Appcode, where some of the people on the team are using Appcode. It's strange that some people do this by writing code in Appcode and then copying and pasting to Xcode to compile, so there's no problem. Of course you can also improve Nuclide,nuclide is the Facebook IDE and currently does not support Swift, but needs to be perfected to support it.

Our solution is to add more frameworks. By breaking the entire application into multiple frameworks, each with a few files, the benefit of doing so is that everything gets faster. Because, according to our experience, the more things inside the frame, the more likely the tool will become problematic.

At the beginning, it is not difficult to define more than 70 or 80 frames. Of course, if you just want to write code and do not need to compile, you can also turn off the indexing function, and some people do.


3. Size of binary files

Again, the size of the binary file. Any app, its size must be controlled within 100M, if exceeded, it must be downloaded via WiFi, so you will encounter some problems. If you have a structure in your app app, the app will get bigger, and if there are structs in the list, they will be created in the stack, causing the application to become larger. At the very beginning, we set the model to the structure, and the resulting binary file seems to be 80M, which is not what we want.

Optional features also increase the size of the file, the surface of these features you can selectively use, but you do not know, the compiler has been in the background silently do a lot of things, the compiler must go to check this part of the code, but also to extract and so on, actually compiled a lot of things.

Generics are another problem we encounter. As long as you are using generics, if you want these generics to become faster, the compiler will make them special and eventually the compiled binaries will become larger.

The library files that the SWIFT runtime relies on are also included in the application, and we compress the library files to a final actual size of only 4.5MB.

Reference

Solution:

You can solve this problem by optimizing settings. Turning on the O-whole-module-optimization optimization level can sometimes make the compilation file smaller and sometimes cause it to become larger, which requires you to know where the compilation is consuming time, so we also do a tool that maps a single symbol to a file, Finally, by combining these files, you can visually see the application's folder structure and the size of each swift file.


4. Start Speed

Startup speed is another tricky thing that we've encountered in our development process. If you've seen the speech at the Apple Global Developer conference, you'll come to the conclusion that SWIFT can achieve a faster start-up, and now it's the exact opposite. Typically, the number of dynamic libraries in a binary file will directly affect the start-up time in Pre-main, and Pre-main and Post-main are determined by both. Pre-main occurs before the main method call, if the number of dynamic libraries is too large, it will take more time.

For example, on an iphone 6s phone, the Swift Runtime Library takes 250 milliseconds to complete their actions, which means that during these 250 milliseconds, you won't be able to return with Swift, which is a lazy phenomenon.

We found that the tool problem we encountered was caused by the creation of more frameworks, the more things you had in the frame, the slower you started.

Reference

Solution Solutions

All the content can be re-linked to the binaries, which is the scenario we're using. We built these frameworks and did post-processing, taking all the symbols out of these frames and linking them to the static binaries, which is how we solved the slow startup scenario.

You may also encounter problems with the Enterprise certificate. If your device has an enterprise certificate, it may take up to 10 seconds for the app to initialize the load, depending on the number of certificates.

You can reduce the time by re-linking, and of course you can increase the post-main time by making some other adjustments.

We are currently trying to use DTrace to probe the access symbols in the boot sequence. Because of the re-linking, so ensure that they are in the correct order, so as to prevent in some of the old devices, do not need to load a large number of pages into memory, but during the startup process you can follow the requirements of some pages read into memory.


the side that makes us feel ashamed

If you take part in the swift summit yesterday or a year ago, you can feel it, and in the process of the demonstration we have a real problem, that is, the compilation speed is very slow, our basic application takes 15-20 minutes to complete clean work.

We were all worried about it, so we consulted everyone on the team: "How big the problem is", we asked: "Based on the problems encountered in the programming process, think about it, in the course of the future development of Uber, which language do you think will be more suitable for iOS development?" ”

This is based on the results of a statistical chart, almost half the half:

The results show that even though Swift has problems with error rates, infinite indexing, and compile speed, they insist on using swift, while the other half choose to switch back to objective-c.

So we've added another question:

"If you implement one or both of the following, you will choose swift and even change your perception of Swift."

If it's just a matter of compiling speed, we can actually solve it.

Compile speed optimization

After figuring out why, the first thing we do is switch back to Swift. Try not to use type judgments in your code, we've developed a script that uses Sourcekit to build all types later, just change the code so that it has all the type information.

Finally, we started combining the files, and we found that by combining all of our 200 models into one file, we could reduce the compilation time from 1 minutes to 35 seconds to just 17 seconds. So we thought, "If we continue to combine everything else into a single file, that's not going to be faster, it's really fun." The reason for this is because the compiler will type-check each individual file, so if you generate 200 processes for the Swift compiler, you need to check 200x for all other files, so combining all of the content into one file makes it faster to compile.

The optimization of the whole module is exactly what we want to do. It compiles all the files into a single file. The whole module optimization problem is optimization, so it's pretty slow. But if you add a user-defined custom flag swift_whole_module_optimization, set it to Yes, and set the optimization level to none, it will complete the full-module optimization without optimization, and it will be super fast.

At the moment our biggest framework is based on core flow, which has 900 files that took four minutes to compile and now takes only 23 seconds to complete. It takes only 23 seconds to compile the largest library, so it doesn't matter if you can't make incremental builds anymore. Most other targets have far fewer files and faster speeds.

Uber is contributing to Facebook's "Buck" and joining Swift's support

If the CPU utilization of the entire module has been optimized to below 30%, then you can consider doing something else. If you use objective-c language for development, then we have to use buck. Buck provides better dependency management, reliable incremental compilation, and remote compilation caching. It was created by Facebook and you might be concerned about it if there was a problem during compilation. We had previously compiled the Objective-c and Android, and finally our cleanup compiles 4 times times faster, our incremental compilation is 20 times times faster because it uses a remote compile cache, so if you're compiling multiple targets and others have compiled the code on other machines, Then it will be available in the remote compilation cache and will only use the file, and it will not recompile anything. On Android, it's faster, like 6 times times faster cleanup compile time, while the incremental version is just fast.

It's not swift, but we're working on it, so we've been working on Facebook to support Swift, and we're starting to try to add swift support when building the Xcode project file, which I think is almost or nearly done now, We've started doing this internally, and now we can use buck to generate the project file based on the folder structure.

Next, we are working to add swift support for the Buck compiler, and this is done in the future by using buck to compile our application. Eventually we want to look at how to integrate swift support that has been added to buck into Xcode, and if the research succeeds, then when you hit Cmd + B, it will not compile with Xcode, but will use buck to compile.

If you use Buck, you now have 6 minutes of compilation time, which can be reduced to 2 minutes or less later. This will essentially solve the problem of swift compile time. All of this can be done in accordance with Buck repo, and you will eventually see swift support coming in.

Uber uses Swift to rewrite app's pit Experience and Solutions (reprint)

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.