Every decision connected with the choice of technology stack is crucial, because it has a huge impact on future system limitations which can be hard to predict. Usually, it is a good approach to stick with some battle-tested solutions, especially when we are working on a typical implementation. Everything can become more complicated when we have limited resources or a specific problem to solve.

Swift gives us a lot of modern features that allow us to write safer and more elegant code. There are web frameworks written entirely in Swift that are dynamically evolving and potentially can become a good alternative for widely used frameworks in Java, JavaScript or Ruby.

Our aim is to test a concept of the full-stack Swift in a real application. In addition, we want to investigate replacing typically used serialization method of JSON with the more modern Protocol Buffers and see how it affects the performance of web services.

Swift on the server

When Apple made Swift an open source software at the end of 2015[1], it became clear that it would open the language to new application areas. Natural consequence was to start the development of new web frameworks in Swift, what would make it possible to implement backend and iOS applications in the same language.

Swift with its strongly typed model and modern features can be a great replacement for the variety of more dynamic languages. Designers of Swift tried to make it safer to write code. Besides the mentioned strong typing, we also get optionals which simplify operating with nullability, redesigned and safer statements – for example the switch statement that has to be exhaustive, error handling and more.

Apple is not alone in showing interest in Swift. IBM seems to invest a lot of resources to integrate Swift with its cloud platform called Bluemix. What is worth mentioning is IBM Swift Sandbox[2], which is still in beta but allows us to run Swift code remotely from a web browser. And, last but not least, Kitura[3], one of the most dynamically developed Swift frameworks. At the time of writing this article there were 4 major web frameworks for Swift: Perfect[4], Kitura, Vapor[5] and Zewo[6]. They all can be run on both macOS and Linux and have strong communities behind. We will focus on Kitura because of its similarity to Express framework[7], great support and easy configuration.

With Swift on the server, it is now possible to become a full-stack developer without half measures in the form of the hybrid application development nor simply dealing with JavaScript stack. It gives us more flexibility to choose well-matched solutions. It also gives an opportunity for iOS developers to test themselves on the server-side programming, giving more options to create a feature-oriented, multidisciplinary team. Moreover it is now easier for iOS developers to create stubs in case of temporary lack of server-side development in the early stage of a project.

Protocol Buffers

Usually, when designing communication between a server and an iOS application, we choose JSON as a default serialization method. It has a lot of advantages: readability, flexibility and good availability of serializers in the majority of most popular web technologies used. In most cases it is sufficient to use JSON, but when we want to send a lot of data in one shot or our services generate a lot of traffic, it can be reasonable to replace JSON with a more efficient method.

Protocol Buffers[8], a method of serializing data developed by Google, has been used internally by the company for about seven years and then publicly published in 2008. It has a lot of advantages compared to JSON, worth mentioning:

  • Protocol Buffers comes with the concept of schema. If we want to serialize data in order to send it through the network, we need to define its data type first.
  • Every message defined by a schema is strongly typed. In contrast to JSON, every field in the schema has its own data type that cannot be changed. No more unspecified changes of data type between revisions of the API!
  • A message can be composed of simple data types or other messages.
  • Fields are numbered, what gives backward compatibility to services using the same schema in different revisions.
  • Data sent by Protocol Buffers has an apparently smaller footprint.

Protocol Buffers can be used with many web technologies. There are serializers, deserializers and code generators for the majority of programing languages including Java, C++, C#, Python, Go, Ruby and JavaScript.

Message definitions

To test the concept of the full-stack development in Swift with the use of Protocol Buffers we created the sample server and the iOS application. They communicated via HTTP so they had to have a common proto schema. We started with defining the schema for our sample server. It was a simple web service to serve a list of bank accounts and transactions.

Every schema definition should start with the version indication. In our example we used Protocol Buffers in version 3. Next we defined three message types: Transaction, Account and AccountList.

Fields in the message are strongly typed and numbered. In the Protocol Buffers documentation[9] you can find a list of available data types. Most of them are self-explanatory: double, float, string, int32, int64, uint64, etc. Numeric tag should be unique within the type definition and is placed after field name and the equality sign.

Defining enums is similar to defining messages, the only difference is that we don’t place a type before a name. Complex data types can be defined inline, just before the use. In our example we defined TransactionType as a enum with two possible values: Credit and Debit so we can distinguish between incomes and outcomes in the transaction list. Arrays can be defined by the repeated keyword which should be placed before a type of elements in the field definition. In our example, we defined transactions as a repeated of Transaction type.

To transform our definitions to the form that could be used by serializers and to generate the managing code we need to install protoc compiler. To use them inside a Swift application we need also the Swift Protobuf library.

First, we install the compiler via Homebrew[10]:

brew install protobuf-swift

Now we can build the Swift Protobuf library. We use the 0.9.903 version so we check out the 0.9.903 tag:

git clone https://github.com/apple/swift-protobuf.git
cd swift-protobuf
git checkout tags/0.9.903
swift build -c release -Xswiftc -static-stdlib

This will create a binary that should be available in the system PATH. We can add its path to ~/.bash_profile file using the export statement or simply copy it to /usr/local/bin:

sudo cp .build/release/protoc-gen-swift /usr/local/bin/

Assuming we keep the message definitions in DataModel.proto, we can generate Swift classes to the Source directory by following command:

protoc --swift_out=Sources/ DataModel.proto

And that’s it! We are free to use our model in web and mobile applications.

Let’s write some code

We started with the server implementation. To simplify the process of creating the project structure, we used Swift Package Manager:

mkdir protobuf-server
cd protobuf-server
swift package init --type executable

To add the required dependencies we edited the Package.swift file:

It is important to use the same Protocol Buffers version in the project as used to generate our models, so we chose the 0.9.903 version here as well.

We can write the server code in any favorite editor, but if our favorite editor is Xcode, then we can also create Xcode project to be able to run our server from the IDE.

swift package update
swift package generate-xcodeproj
open protobuf-server.xcodeproj

We wanted our server application to load account and transactions information once on the boot to minimize latency not related with serialization and networking. We used Ruby and some handy library called faker to generate fake financial data. The script generates CSV documents to be easily loaded into the web application.

We wanted our stub service to serve a list of two accounts with a different number of transactions to see how the response times depend on the payload size. So we generated two collections of transaction with 1000 and 10000 transactions respectively.

The data loading process is realized by DataLoadHelper class, and it simply reads the corresponding CSV files and return them as Swift objects. See the sample method which loads the transaction list below.

Unfortunately, at present there is no option to copy assets to the main Bundle from the Swift Package Manager level so we created a separate bundle just for the generated CSV files:

For better comparison with JSON serialization, the Accept header was used. Thanks to that we can control the format in which we want data to be serialized from the iOS application level. To manage this feature we created HttpHeaderHelper that can check the headers and set the desired content type. If a user of our API won’t set Accept headers, we want the JSON serialization to be used as a more common one.

Starting point for every Kitura server is simply the main.swift file. It is a place where we can initialize the Router object, prepare our stub objects, add an HTTP server to the Router and start the Kitura’s runloop.

As you may notice, we create the router object by our custom object RouterCreator and run our server on the port 8080.

Kitura’s router is responsible for defining our endpoint’s routes. Besides the /accountList endpoint that responds with the list of all accounts we also created one endpoint that allows us to pick one account with transaction list. It is available for the route of "/account/:accountId" where accountId should be a UInt64 number.

All classes generated by Protocol Buffers are of type SwiftProtobuf.Message which has a bunch of serialization and deserialization methods. To get data serialized to Protocol Buffers we just call serializedData() method on an object, and to serialize an object to JSON we can use jsonUTF8Data() method.

If we created a Xcode project for the application and want to run it directly from IDE, we can do that by using a standard ⌘ + R (Command + R) shortcut. It is important to choose our executable schema, because it is not selected automatically. If everything is fine, it should look like in the Fig 1.

Server running
Fig 1. Kitura server running from Xcode IDE

If you don’t want to generate an Xcode project then you can build and run the server from Terminal:

swift build
.build/debug/protobuf-server

Now we can open a browser and go to http://localhost:8080 address to see the Kitura’s placeholder page.

Kitura running
Fig 2. Kitura is working

The article includes only fragments of the original solution. Full source code of the server application is available on Github[11].

iOS application

The source code of the iOS application is also available on Github[11]. We decided to use Alamofire library to simplify the networking layer and use some built-in diagnostic features. To manage dependencies we used Cocoapods.

We had to use the same Swift Protobuf version as in the code generation step and the server application. Otherwise, the generated code would not correspond to the Protocol Buffers version used in the application.

The process of data deserialization on the client side is also easy manageable. We simply get the data from the responseData property and pass it to corresponding SwiftProtobuf.Message object.

We also wanted to know how long it takes for the server to respond, so we also got request duration times. The totalDuration is the time interval in seconds from the time the request started to the time response serialization completed. The requestDuration is the time interval in seconds from the time the request started to the time the request completed.

To run the iOS application we should download all dependencies and open the project using a workspace:

pod install
open ProtobufSampleApp.xcworkspace/

Fig 3 includes screenshots of the sample application where the communication was JSON and Protocol Buffers serialized respectively.

iOS emulator transactions JSON and Protobuf
Fig 3. List of 10000 transactions serialized with JSON (left) and Protobuf (right)

As you may notice, request and total duration times for Protobuf are significantly shorter than using JSON serialization (about 3 times). We run the app in the same environment 20 times to get the average duration times for one client.

json serialization

It gives us the average values of 0.56677 seconds for Protobuf and 1.766665 seconds for JSON. The table below contains the summary of payload sizes of our test requests.

summary of payload sizes of our test requests

For the sample data, the payloads for Protocol Buffers are two times smaller than JSON’s in every case. Such a kind of saving is especially important for mobile devices where the web services should respond quickly.

Load tests

We also wanted to test our sample server in more extreme conditions to look how it would behave for 20, 50, 100, 200 and 300 users using our API in the local environment simultaneously. We chose to use Gatling[12] framework as it is very easy to record and run load tests locally.

Gatling is available as a zip archive containing binaries and dependencies, so it is ready to use just after download. The bin directory contains two executable scripts: gatling.sh and recorder.sh. Gatling Recorder is a handy tool for recording simulators in a form of Scala[13] script.

Gatling recorder
Fig 4. Gatling Recorder

The recorder works as a local proxy that saves all requests going through it. After clicking on the ‘Start’ button, the recorder listens on a port defined in the ‘Listening port’ field. Gatling scripts are saved in the output folder after recording. We chose to configure Firefox browser to use Gatling Recorder, so we went to Preferences, Advanced, Network tab, and clicked on ‘Settings…’ button in the Connection section. We wanted to configure proxy manually and chose the Gatling Recorder’s port of 8000, as in the Fig 5.

Firefox proxy
Fig 5. Firefox configuration

After configuring the browser to use the Gatling’s proxy, and starting the recorder, we used our API via Firefox browser to cover all our endpoints in a single session:

  • localhost:8080/account/1
  • localhost:8080/account/2
  • localhost:8080/accountList

To prepare the Protobuf scenario, we repeated the procedure from starting new session in Gatling Recorder. We used ModifyHeaders plugin to modify Accept header, as in the Fig 6.

Firefox ModifyHeaders plugin
Fig 6. Accept header modified by ModifyHeaders plugin

Having two simulations in the output folder allows us to create a Gatling project and adapt them to cover our cases of using the API simultaneously by more than one user. To do that, we used SBT[14] and IntelliJ IDEA[15]. SBT is a build tool for Scala that simplifies all processes required for building Scala projects including dependency management, testing and deployment.

SBT and Scala are available as an IntelliJ plugin. After creating SBT project, we added required dependencies in the build.sbt file:

In the project/plugins.sbt file we also added Gatling Plugin to be available in the build process:

The simulations generated by Gatling Recorder had to be placed inside scr/test/scala, and are as follows:

TransactionsProtobufSimulator is analogical, but has a different acceptHeader. By increasing and decreasing the atOnceUsers method parameter we can change the number of users using our API in the same time during the simulation. The source code of the whole Gatling project is available on GitHub[11].

To run the simulation we open the terminal and run the sbt from the project level:

sbt

Then we run the test target:

gatling:test

Gatling’s reports are generated into the target/gatling folder and are in the HTML format as in the Fig 7 and Fig 8.

Gatling report JSON
Fig 7. Gatling’s report for JSON and 100 users
Gatling report Protobuf
Fig 8. Gatling’s report for Protobuf and 100 users

As you may notice, Protocol Buffers endpoints have shorter duration times, and in case of 100 users using the API simultaneously, we got a timeout for JSON serialization for 66 requests. Reports also include duration of the whole simulation, number of successful and unsuccessful requests, number of requests per second, response times for various different percentiles.

They also include charts for the number of active users along the simulation, response time distribution, response time percentiles over time, number of requests per second and number of responses per second. The summary of the simulations is shown in the tables below.

The summary of the simulations

The JSON endpoints needed more time to respond and failed earlier. The Protocol Buffers endpoints could handle more requests per second. They also had significantly smaller minimal and mean response times. More interestingly, the minimum response time before timeouts decreased from 178 to 107 ms, and maximum response times increased less dynamically.

Conclusions

Open Source Swift created a great opportunity for the development of new frameworks for web applications. We observe a stable growth of these solutions and hope they will be more commonly used in the nearest future.

Protocol Buffers is a good alternative for the JSON-based web services. Because of its schema approach, it allows us to design safer web services with less effort than using JSON serialization. Smaller payloads also increase the number of requests per second that can be handle by the server.

We created a working solution in Kitura framework using the serialization of JSON and Protocol Buffers and a basic mechanism that allows us to switch serializer from the iOS application side.

Despite the fact that Swift frameworks are still in dynamic development, and the language itself is also not free from significant changes, it can be considered as a good alternative for the battle-tested technologies in case of working on experimental solutions.

References

1. https://developer.apple.com/swift/blog/?id=34

2. https://swift.sandbox.bluemix.net/#/repl

3. http://www.kitura.io

4. http://perfect.org

5. http://vapor.codes

6. http://zewo.io

7. https://expressjs.com

8. https://developers.google.com/protocol-buffers

9. https://developers.google.com/protocol-buffers/docs/proto

10. https://brew.sh

11. ⌃ a b c https://github.com/codete/protobuf-samples

12. http://gatling.io

13. https://www.scala-lang.org

14. http://www.scala-sbt.org

15. https://www.jetbrains.com/idea/download

michal.cichon

A software engineer and a big enthusiast of the Apple's ecosystem. He likes solving complex problems with simple solutions.