Performance-Profiling Swift on Linux: Getting Started

Learn how to profile Server-Side Swift with perf on Linux. You’ll discover the basic principles of profiling and how to view events, call-graph-traces and perform basic analysis. By kelvin ma.

Leave a rating/review
Download materials
Save for later
Share

It happens to app developers. Sooner or later, every application you build hits its natural limits, and you’ll have to optimize for performance. If you’re developing on macOS, Instruments is the tool for the job. But if you’re developing on Linux, you might be wondering what to use instead.

Note: Perf works differently across architectures such as Intel and Apple Silicon. There are also differences when running Docker on macOS and Linux. Any differences you might encounter when following this tutorial will be called out.

Fortunately, Linux developers aren’t strangers to performance optimization. In this tutorial, you’ll profile a cryptocurrency trading application, using the same workflow you might apply to one of your own projects. You’ll use the perf tool to collect data about the performance of the application and generate a performance profile that shows which parts of the application are slow, and why they are slow. Along the way, you’ll learn:

  • How to use perf record to gather data about an application.
  • How to use perf script to view columns of performamce data.
  • How to read and interpret performance data.
  • How to configure perf to produce performance profiles tailored specifically for Swift binaries.
  • How to use swift demangle to post-process a performance profile.

Let’s get started!

Note: This tutorial assumes you know how to clone a Git repository, run terminal commands and run a Docker container. It also assumes you know how to build a simple Swift Package Manager project. For an overview of the Swift Package Manager, check our Introduction to the Swift Package Manager tutorial. For a gentle introduction to Docker, see our Docker on macOS: Getting Started tutorial.

Getting Started

First, click the Download Materials button at the top or bottom of this tutorial. This archive contains a simple application called crypto-bot. Unzip the file, and navigate to the starter project directory. It contains a Swift Package Manager project with a Package.swift manifest file. It also contains the dockerfile you’ll use to build the Docker image for this tutorial.

The Data File

Alongside the manifest and the dockerfile is the ftx.json data file. It contains serialized market data from the FTX cryptocurrency exchange. Open this file in a text file viewer. For example, to view it with the cat tool, run the following command in a terminal:

cat ftx.json
Warning: This file is large! Use a viewer designed for displaying large text files.

You should see a stream of cryptocurrency market data formatted as JSON. This is real data, captured from FTX’s public market data API. Although the file is over 8 MB in size, it represents just a few seconds of trading activity from a few markets in a single exchange. This should give you a sense of how much data real-world trading applications handle!

The Trading Application

Start building the Docker image by running the following command in the starter project directory:

docker build . --file dockerfile --tag swift-perf

This might take a few minutes to complete.

Meanwhile, look at the files in Sources/crypto-bot. For the purposes of this tutorial, we’ve removed all the components of crypto-bot not related to parsing or decoding market data. Open example.swift and find Main.main(), reproduced below:

static func main() throws {
  // 1
  let data = try File.read([UInt8].self, from: "ftx.json")
  // 2
  var input = ParsingInput<Grammar.NoDiagnostics<[UInt8]>>(data)

  var updates: [String: Int] = [:]
  // 3
  while let json = input.parse(as: JSON.Rule<Array<UInt8>.Index>.Root?.self) {
    // 4
    guard let message = decode(message: json) else {
      continue
    }
    updates[message.market, default: 0] +=
      message.orderbook.bids.count + message.orderbook.asks.count
  }
  // 5
  try input.parse(as: Grammar.End<Array<UInt8>.Index, UInt8>.self)
  // 6
  for (market, updates) in updates.sorted(by: { $0.value > $1.value }) {
    print("\(market): \(updates) update(s)")
  }
}

Key points about this function:

  1. This loads the market data file as a [UInt8] array. It’s faster than loading it as a String because the file is UTF-8-encoded, and the JSON module supports parsing JSON directly from UTF-8 data.
  2. This creates a ParsingInput view over the [UInt8] array and disables parsing diagnostics. These APIs are part of the JSON module.
  3. This loop tries to parse one complete JSON message at a time until it exhausts the parsing input. These APIs are also part of the JSON module.
  4. This attempts to decode the fields of an FTX.Message instance from a parsed JSON value, skipping it if it’s not a valid FTX.Message.
  5. This asserts that the parsing loop exited because the parser reached the end of the input — not because it encountered a parsing error.
  6. This prints out the number of events recorded in each cryptocurrency market.

Below main() is a function called decode(message:), reproduced below:

@inline(never)
func decode(message json: JSON) -> FTX.Message? {
    try? .init(from: json)
}

This function takes a parameter of type JSON, which is a Decoder, and passes it to FTX.Message’s Decodable implementation. It’s marked @inline(never) to make it easier to measure its performance.

Below decode(message:) is a similar function named decodeFast(message:). As its name suggests, it contains a significantly more efficient JSON decoding implementation. At the end of this tutorial, you will compare its performance to that of decode(message:).

The Supporting Code

The other files in Sources/crypto-bot are less important to this tutorial. Here’s an overview:

  • ftx.swift: This contains the definition of FTX.Message, which models a kind of market event called an orderbook update. Most of the market events in ftx.json are orderbook updates. The ftx.swift file also directs the compiler to synthesize a Codable implementation for FTX.Message.
  • decimal.swift: This defines a simple decimal type and directs the compiler to synthesize a Codable implementation for it.
  • system.swift: This contains helper code that main() uses to load ftx.json from disk.

Running the Application

If the Docker image has finished building, start a container with it:

docker run -it --rm --privileged --name swift-perf-instance -v $PWD:/crypto-bot swift-perf

This is a normal Docker run command, except it contains the option --privileged. You will need this because performance profiling uses specialized hardware in your CPU, which the default Docker containers can’t access.

Your prompt should look like this:

/crypto-bot$

Note the shell has already cd in the project directory.

Within the container, build crypto-bot with the Swift Package Manager. Ensure you compile it in release mode, because running performance benchmarks on unoptimized code would not be meaningful.

swift build -c release

Try running crypto-bot in the container. It should run for a second or two, and then print a long list of markets and orderbook update counts:

.build/release/crypto-bot

Output:

FTM-PERP: 7094 update(s)
AXS-PERP: 3595 update(s)
FTT-PERP: 3458 update(s)
...
MEDIA-PERP: 64 update(s)
Note: If you see warnings about missing symbols when running on Apple Silicon or ARM then you can safely ignore these.

That’s fast, but remember that ftx.json only holds a few seconds of market data. The application is nowhere near fast enough to keep up with the exchange in real time, which means you are going to have to optimize.